ソースを参照

Add sqlite and 7zip support in agi

Toby Chui 1 週間 前
コミット
da8d3e002c
7 ファイル変更1392 行追加107 行削除
  1. 27 14
      src/go.mod
  2. 59 24
      src/go.sum
  3. 584 0
      src/mod/agi/agi.7z.go
  4. 336 0
      src/mod/agi/agi.sqlite.go
  5. 136 69
      src/mod/agi/agi.zip.go
  6. 249 0
      src/mod/agi/agi_sqlite_test.go
  7. 1 0
      src/mod/agi/moduleManager.go

+ 27 - 14
src/go.mod

@@ -1,8 +1,6 @@
 module imuslab.com/arozos
 
-go 1.24.0
-
-toolchain go1.24.1
+go 1.25.0
 
 require (
 	github.com/aws/aws-sdk-go-v2 v1.41.7
@@ -16,6 +14,7 @@ require (
 	github.com/fclairamb/ftpserverlib v0.27.0
 	github.com/fogleman/fauxgl v0.0.0-20250110135958-abf826acbbbd
 	github.com/gabriel-vasile/mimetype v1.4.10
+	github.com/glebarez/go-sqlite v1.22.0
 	github.com/go-git/go-billy/v5 v5.6.2
 	github.com/go-git/go-git/v5 v5.16.5
 	github.com/go-ldap/ldap v3.0.3+incompatible
@@ -40,17 +39,17 @@ require (
 	github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef
 	github.com/studio-b12/gowebdav v0.11.0
 	gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6
-	golang.org/x/crypto v0.46.0
+	golang.org/x/crypto v0.50.0
 	golang.org/x/image v0.33.0
-	golang.org/x/oauth2 v0.32.0
-	golang.org/x/sync v0.19.0
+	golang.org/x/oauth2 v0.34.0
+	golang.org/x/sync v0.20.0
 )
 
 require (
 	dario.cat/mergo v1.0.2 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
 	github.com/ProtonMail/go-crypto v1.3.0 // indirect
-	github.com/andybalholm/brotli v1.2.0 // indirect
+	github.com/andybalholm/brotli v1.2.1 // indirect
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
 	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
@@ -65,10 +64,14 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 // indirect
 	github.com/aws/smithy-go v1.25.1 // indirect
+	github.com/bodgit/plumbing v1.3.0 // indirect
+	github.com/bodgit/sevenzip v1.6.4 // indirect
+	github.com/bodgit/windows v1.0.1 // indirect
 	github.com/cenkalti/backoff v2.2.1+incompatible // indirect
 	github.com/cloudflare/circl v1.6.3 // indirect
 	github.com/cyphar/filepath-securejoin v0.5.0 // indirect
 	github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/fclairamb/go-log v0.6.0 // indirect
 	github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 // indirect
@@ -76,32 +79,42 @@ require (
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/golang/snappy v1.0.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
 	github.com/gopherjs/gopherjs v1.17.2 // indirect
 	github.com/gorilla/securecookie v1.1.2 // indirect
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/kevinburke/ssh_config v1.4.0 // indirect
-	github.com/klauspost/compress v1.18.2 // indirect
+	github.com/klauspost/compress v1.18.6 // indirect
 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
 	github.com/klauspost/pgzip v1.2.6 // indirect
 	github.com/kr/fs v0.1.0 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/miekg/dns v1.1.68 // indirect
 	github.com/nwaples/rardecode v1.1.3 // indirect
-	github.com/pierrec/lz4/v4 v4.1.22 // indirect
+	github.com/pierrec/lz4/v4 v4.1.26 // indirect
 	github.com/pjbgf/sha1cd v0.5.0 // indirect
+	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/sergi/go-diff v1.4.0 // indirect
 	github.com/skeema/knownhosts v1.3.2 // indirect
+	github.com/stangelandcl/ppmd v0.1.0 // indirect
 	github.com/ulikunitz/xz v0.5.15 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
 	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
 	gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
-	golang.org/x/mod v0.30.0 // indirect
-	golang.org/x/net v0.48.0 // indirect
-	golang.org/x/sys v0.39.0 // indirect
-	golang.org/x/text v0.32.0 // indirect
-	golang.org/x/tools v0.39.0 // indirect
+	go4.org v0.0.0-20260112195520-a5071408f32f // indirect
+	golang.org/x/mod v0.35.0 // indirect
+	golang.org/x/net v0.53.0 // indirect
+	golang.org/x/sys v0.43.0 // indirect
+	golang.org/x/text v0.37.0 // indirect
+	golang.org/x/tools v0.44.0 // indirect
 	gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
 	gopkg.in/sourcemap.v1 v1.0.5 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
+	modernc.org/libc v1.37.6 // indirect
+	modernc.org/mathutil v1.6.0 // indirect
+	modernc.org/memory v1.7.2 // indirect
+	modernc.org/sqlite v1.28.0 // indirect
 )

+ 59 - 24
src/go.sum

@@ -6,8 +6,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
 github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
 github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
 github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
-github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
-github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
+github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
+github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@@ -50,6 +50,12 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.1 h1:F/M5Y9I3nwr2IEpshZgh1GeHpOIt
 github.com/aws/aws-sdk-go-v2/service/sts v1.42.1/go.mod h1:mTNxImtovCOEEuD65mKW7DCsL+2gjEH+RPEAexAzAio=
 github.com/aws/smithy-go v1.25.1 h1:J8ERsGSU7d+aCmdQur5Txg6bVoYelvQJgtZehD12GkI=
 github.com/aws/smithy-go v1.25.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
+github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
+github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
+github.com/bodgit/sevenzip v1.6.4 h1:iHiVJfxbrB6RF4X+snI2MpVgNBKmVfGaTqZGNlMQIU0=
+github.com/bodgit/sevenzip v1.6.4/go.mod h1:ZtNi5KNgHXeXg1G7WiF0IWSuFE2eG6lt/cTGlvuirO0=
+github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
+github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
 github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
@@ -68,6 +74,8 @@ github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44am
 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
 github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
 github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
 github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -85,6 +93,8 @@ github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9t
 github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
 github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
 github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
+github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
+github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
 github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -114,6 +124,10 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
 github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
 github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
 github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
@@ -129,6 +143,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
 github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -139,8 +155,8 @@ github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PW
 github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
-github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
+github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
+github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
 github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -158,6 +174,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
 github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
 github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
@@ -175,8 +193,8 @@ github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeD
 github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb h1:JF9kOhBBk4WPF7luXFu5yR+WgaFm9L/KiHJHhU9vDwA=
 github.com/oov/psd v0.0.0-20220121172623-5db5eafcecbb/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8=
 github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
-github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
+github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
 github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
 github.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=
 github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
@@ -187,6 +205,8 @@ github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
 github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/robertkrimen/otto v0.5.1 h1:avDI4ToRk8k1hppLdYFTuuzND41n37vPGJU7547dGf0=
 github.com/robertkrimen/otto v0.5.1/go.mod h1:bS433I4Q9p+E5pZLu7r17vP6FkE6/wLxBdmKjoqJXF8=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -208,12 +228,16 @@ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiY
 github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
 github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
+github.com/stangelandcl/ppmd v0.1.0 h1:0gOKtSdWyXxpRW55H/dZ3AgaJqjrtlIrAvsokFwp7ug=
+github.com/stangelandcl/ppmd v0.1.0/go.mod h1:Rrv7M+/2P5jYr/GMLhBl7Ug3uJ1bUiVzr5LbbaV6xgY=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU=
@@ -233,6 +257,8 @@ gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWj
 gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA=
 gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6 h1:WKij6HF8ECp9E7K0E44dew9NrRDGiNR5u4EFsXnJUx4=
 gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6/go.mod h1:vhrHTGDh4YR7wK8Z+kRJ+x8SF/6RUM3Vb64Si5FD0L8=
+go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
+go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -243,8 +269,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
 golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
-golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
+golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -256,8 +282,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
-golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
+golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
+golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -272,10 +298,10 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
-golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
-golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
-golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
+golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -283,8 +309,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
+golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -298,13 +324,14 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
-golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -314,8 +341,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
 golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
-golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
-golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
+golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
+golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -326,8 +353,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
-golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
+golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -335,8 +362,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
-golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
-golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
+golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -355,3 +382,11 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
+modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
+modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=

+ 584 - 0
src/mod/agi/agi.7z.go

@@ -0,0 +1,584 @@
+package agi
+
+/*
+	AGI 7-Zip Library Extension
+
+	Adds 7z archive support to the ziplib namespace.
+	Call inject7zLibFunctions from injectZipFileLibFunctions (after the main
+	vm.Run that creates the ziplib object) so the bindings can extend it.
+
+	Depends on github.com/bodgit/sevenzip (BSD-3-Clause).
+*/
+
+import (
+	"encoding/json"
+	"errors"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/bodgit/sevenzip"
+	"github.com/robertkrimen/otto"
+	"imuslab.com/arozos/mod/agi/static"
+	"imuslab.com/arozos/mod/filesystem"
+	"imuslab.com/arozos/mod/filesystem/arozfs"
+)
+
+// inject7zLibFunctions registers all 7z functions into the ziplib JS namespace.
+// Must be called after the main vm.Run block that creates `var ziplib = {}`.
+func (g *Gateway) inject7zLibFunctions(payload *static.AgiLibInjectionPayload) {
+	vm := payload.VM
+	u := payload.User
+	scriptFsh := payload.ScriptFsh
+
+	// list7zFileDir(srcVpath, dirPath) → string[]
+	// List the immediate children of a directory inside a 7z archive.
+	vm.Set("_ziplib_list7zFileDir", func(call otto.FunctionCall) otto.Value {
+		zipVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		dirPath, err := call.Argument(1).ToString()
+		if err != nil {
+			dirPath = ""
+		}
+
+		zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
+		if !u.CanRead(zipVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
+		}
+
+		_, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		filelist, err := szListDir(zipRpath, dirPath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		reply, _ := vm.ToValue(filelist)
+		return reply
+	})
+
+	// list7zFileContents(srcVpath) → JSON string (tree)
+	// Return the full JSON tree of all entries in a 7z archive.
+	vm.Set("_ziplib_list7zFileContents", func(call otto.FunctionCall) otto.Value {
+		zipVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
+		if !u.CanRead(zipVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
+		}
+
+		_, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		jsonStr, err := sz7zListContentsJSON(zipRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		reply, _ := vm.ToValue(jsonStr)
+		return reply
+	})
+
+	// getFileFrom7z(srcVpath, filePathIn7z) → vpath of extracted file in tmp:/
+	// Extract a single file from a 7z archive to a temporary location.
+	vm.Set("_ziplib_getFileFrom7z", func(call otto.FunctionCall) otto.Value {
+		zipVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		fileIn7z, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
+		if !u.CanRead(zipVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
+		}
+
+		_, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		r, err := sevenzip.OpenReader(zipRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		defer r.Close()
+
+		var target *sevenzip.File
+		for _, f := range r.File {
+			if filepath.ToSlash(f.Name) == filepath.ToSlash(fileIn7z) {
+				target = f
+				break
+			}
+		}
+		if target == nil {
+			g.RaiseError(errors.New("File not found in archive: " + fileIn7z))
+			return otto.NullValue()
+		}
+
+		tmpFsh, err := u.GetFileSystemHandlerFromVirtualPath("tmp:/")
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		tmpFilename := arozfs.Base(fileIn7z)
+		tmpVpath := "tmp:/" + tmpFilename
+		tmpRpath, _ := tmpFsh.FileSystemAbstraction.VirtualPathToRealPath(tmpVpath, u.Username)
+
+		rc, err := target.Open()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		defer rc.Close()
+
+		if err = tmpFsh.FileSystemAbstraction.WriteStream(tmpRpath, rc, 0755); err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		u.SetOwnerOfFile(tmpFsh, tmpVpath)
+		reply, _ := vm.ToValue(tmpVpath)
+		return reply
+	})
+
+	// extract7zFile(srcVpath, destVpath) → bool
+	// Extract all files from a 7z archive to destVpath.
+	vm.Set("_ziplib_extract7zFile", func(call otto.FunctionCall) otto.Value {
+		srcVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destVpath, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		srcVpath = static.RelativeVpathRewrite(scriptFsh, srcVpath, vm, u)
+		destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
+
+		if !u.CanRead(srcVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+srcVpath))
+		}
+		if !u.CanWrite(destVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
+		}
+
+		_, srcRpath, err := static.VirtualPathToRealPath(srcVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		if err = sz7zExtractAll(srcRpath, destFsh, destRpath); err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		destFsh.FileSystemAbstraction.Walk(destRpath, func(path string, info os.FileInfo, err error) error {
+			if err == nil && !info.IsDir() {
+				vp, e := static.RealpathToVirtualpath(destFsh, path, u)
+				if e == nil {
+					u.SetOwnerOfFile(destFsh, vp)
+				}
+			}
+			return nil
+		})
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// extractPartial7z(srcVpath, filePathsIn7z, destVpath) → bool
+	// Extract selected files/folders from a 7z archive; strips parent prefix for
+	// folder selections, extracts flat for file selections (same semantics as
+	// extractPartialZip).
+	vm.Set("_ziplib_extractPartial7z", func(call otto.FunctionCall) otto.Value {
+		zipVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		pathsObj, err := call.Argument(1).Export()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destVpath, err := call.Argument(2).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
+		destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
+
+		targetPaths := szParsePathsArg(pathsObj)
+		if targetPaths == nil {
+			g.RaiseError(errors.New("invalid paths format"))
+			return otto.FalseValue()
+		}
+
+		if !u.CanRead(zipVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
+		}
+		if !u.CanWrite(destVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Write access denied: "+destVpath))
+		}
+
+		_, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destFsh, destRpath, err := static.VirtualPathToRealPath(destVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		r, err := sevenzip.OpenReader(zipRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		defer r.Close()
+
+		for _, f := range r.File {
+			outRelPath := szMatchPartialPath(filepath.ToSlash(f.Name), targetPaths)
+			if outRelPath == "" {
+				continue
+			}
+
+			outPath := filepath.Join(destRpath, filepath.FromSlash(outRelPath))
+			if f.FileInfo().IsDir() {
+				destFsh.FileSystemAbstraction.MkdirAll(outPath, 0755)
+				continue
+			}
+			destFsh.FileSystemAbstraction.MkdirAll(filepath.Dir(outPath), 0755)
+
+			rc, err := f.Open()
+			if err != nil {
+				continue
+			}
+			destFsh.FileSystemAbstraction.WriteStream(outPath, rc, 0755)
+			rc.Close()
+
+			vp, err := static.RealpathToVirtualpath(destFsh, outPath, u)
+			if err == nil {
+				u.SetOwnerOfFile(destFsh, vp)
+			}
+		}
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	// get7zFileInfo(srcVpath) → JSON string {fileCount, dirCount, totalUncompressedSize, totalCompressedSize}
+	vm.Set("_ziplib_get7zFileInfo", func(call otto.FunctionCall) otto.Value {
+		zipVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
+		if !u.CanRead(zipVpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Read access denied: "+zipVpath))
+		}
+
+		_, zipRpath, err := static.VirtualPathToRealPath(zipVpath, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		jsonStr, err := sz7zGetFileInfo(zipRpath)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+		reply, _ := vm.ToValue(jsonStr)
+		return reply
+	})
+
+	vm.Run(`
+		// 7z-specific functions
+		ziplib.extract7zFile         = _ziplib_extract7zFile;         // Extract all files from a 7z archive
+		ziplib.list7zFileDir         = _ziplib_list7zFileDir;         // List immediate children of a dir in 7z
+		ziplib.list7zFileContents    = _ziplib_list7zFileContents;    // Full JSON tree of a 7z archive
+		ziplib.getFileFrom7z         = _ziplib_getFileFrom7z;         // Extract single file from 7z to tmp:/
+		ziplib.extractPartial7z      = _ziplib_extractPartial7z;      // Extract selected files/folders from 7z
+		ziplib.get7zFileInfo         = _ziplib_get7zFileInfo;         // Metadata (counts, sizes) of a 7z
+	`)
+}
+
+// ── Package-level helpers (shared between inject7zLibFunctions and the
+//    dispatch branches added to existing ziplib functions) ──────────────────
+
+// szListDir returns the immediate children of dirPath inside the 7z at rpath.
+// Handles archives without explicit directory entries by inferring dirs from
+// file paths (common in 7z).
+func szListDir(rpath, dirPath string) ([]string, error) {
+	r, err := sevenzip.OpenReader(rpath)
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+
+	dirPath = filepath.ToSlash(strings.TrimPrefix(dirPath, "/"))
+	if dirPath != "" && !strings.HasSuffix(dirPath, "/") {
+		dirPath += "/"
+	}
+
+	var filelist []string
+	dirExists := dirPath == ""
+	seenItems := make(map[string]bool)
+
+	for _, f := range r.File {
+		fName := filepath.ToSlash(f.Name)
+
+		if dirPath == "" {
+			parts := strings.Split(fName, "/")
+			if len(parts) == 0 {
+				continue
+			}
+			item := parts[0]
+			if item == "" || seenItems[item] {
+				continue
+			}
+			seenItems[item] = true
+			if len(parts) > 1 || f.FileInfo().IsDir() {
+				filelist = append(filelist, item+"/")
+			} else {
+				filelist = append(filelist, item)
+			}
+		} else if strings.HasPrefix(fName, dirPath) {
+			dirExists = true
+			relPath := strings.TrimPrefix(fName, dirPath)
+			if relPath == "" {
+				continue
+			}
+			parts := strings.Split(relPath, "/")
+			item := parts[0]
+			if item == "" || seenItems[item] {
+				continue
+			}
+			seenItems[item] = true
+			if len(parts) > 1 || strings.HasSuffix(relPath, "/") || f.FileInfo().IsDir() {
+				filelist = append(filelist, item+"/")
+			} else {
+				filelist = append(filelist, item)
+			}
+		}
+	}
+
+	if dirPath != "" && !dirExists {
+		return nil, errors.New("Directory not found in archive: " + dirPath)
+	}
+	return filelist, nil
+}
+
+// sz7zListContentsJSON builds a JSON tree (same schema as listZipFileContents)
+// from all entries in the 7z archive at rpath.
+func sz7zListContentsJSON(rpath string) (string, error) {
+	r, err := sevenzip.OpenReader(rpath)
+	if err != nil {
+		return "", err
+	}
+	defer r.Close()
+
+	type Node struct {
+		Name     string           `json:"name"`
+		IsDir    bool             `json:"isDir"`
+		Size     int64            `json:"size"`
+		Children map[string]*Node `json:"children,omitempty"`
+	}
+	root := &Node{Name: "/", IsDir: true, Children: make(map[string]*Node)}
+
+	for _, f := range r.File {
+		parts := strings.Split(filepath.ToSlash(f.Name), "/")
+		current := root
+		for i, part := range parts {
+			if part == "" {
+				continue
+			}
+			isLast := i == len(parts)-1
+			if current.Children == nil {
+				current.Children = make(map[string]*Node)
+			}
+			if _, exists := current.Children[part]; !exists {
+				current.Children[part] = &Node{
+					Name:  part,
+					IsDir: !isLast || f.FileInfo().IsDir(),
+				}
+				if isLast && !f.FileInfo().IsDir() {
+					current.Children[part].Size = f.FileInfo().Size()
+				}
+			}
+			current = current.Children[part]
+		}
+	}
+
+	data, err := json.Marshal(root)
+	if err != nil {
+		return "", err
+	}
+	return string(data), nil
+}
+
+// sz7zGetFileInfo returns JSON metadata for the 7z archive at rpath.
+func sz7zGetFileInfo(rpath string) (string, error) {
+	r, err := sevenzip.OpenReader(rpath)
+	if err != nil {
+		return "", err
+	}
+	defer r.Close()
+
+	type Info struct {
+		FileCount             int   `json:"fileCount"`
+		DirCount              int   `json:"dirCount"`
+		TotalUncompressedSize int64 `json:"totalUncompressedSize"`
+		TotalCompressedSize   int64 `json:"totalCompressedSize"` // always 0: 7z uses solid compression
+	}
+	info := Info{}
+	for _, f := range r.File {
+		if f.FileInfo().IsDir() {
+			info.DirCount++
+		} else {
+			info.FileCount++
+			info.TotalUncompressedSize += f.FileInfo().Size()
+		}
+	}
+
+	data, err := json.Marshal(info)
+	if err != nil {
+		return "", err
+	}
+	return string(data), nil
+}
+
+// sz7zExtractAll extracts every entry in the 7z at srcRpath into destRpath
+// using the provided filesystem handler.
+func sz7zExtractAll(srcRpath string, destFsh *filesystem.FileSystemHandler, destRpath string) error {
+	r, err := sevenzip.OpenReader(srcRpath)
+	if err != nil {
+		return err
+	}
+	defer r.Close()
+
+	for _, f := range r.File {
+		fName := filepath.ToSlash(f.Name)
+		outPath := filepath.Join(destRpath, filepath.FromSlash(fName))
+
+		if f.FileInfo().IsDir() {
+			destFsh.FileSystemAbstraction.MkdirAll(outPath, 0755)
+			continue
+		}
+		destFsh.FileSystemAbstraction.MkdirAll(filepath.Dir(outPath), 0755)
+
+		rc, err := f.Open()
+		if err != nil {
+			continue
+		}
+		destFsh.FileSystemAbstraction.WriteStream(outPath, rc, 0755)
+		rc.Close()
+	}
+	return nil
+}
+
+// szParsePathsArg converts the second argument of extractPartial7z / extractPartialZip
+// (JS array or raw JSON string) into a []string.  Returns nil on unrecognised type.
+func szParsePathsArg(pathsObj interface{}) []string {
+	var result []string
+	switch v := pathsObj.(type) {
+	case []interface{}:
+		for _, s := range v {
+			if str, ok := s.(string); ok {
+				result = append(result, filepath.ToSlash(strings.TrimPrefix(str, "/")))
+			}
+		}
+	case string:
+		v = strings.TrimSpace(v)
+		if strings.HasPrefix(v, "[") {
+			var arr []string
+			if json.Unmarshal([]byte(v), &arr) == nil {
+				for _, s := range arr {
+					result = append(result, filepath.ToSlash(strings.TrimPrefix(s, "/")))
+				}
+				return result
+			}
+		}
+		if v != "" {
+			result = append(result, filepath.ToSlash(strings.TrimPrefix(v, "/")))
+		}
+	default:
+		return nil
+	}
+	return result
+}
+
+// szMatchPartialPath returns the output-relative path for fName given the list
+// of selected targetPaths, applying the same strip-parent-for-folders /
+// flatten-for-files semantics as extractPartialZip.  Returns "" if not matched.
+func szMatchPartialPath(fName string, targetPaths []string) string {
+	for _, target := range targetPaths {
+		if target == "" || target == "/" {
+			return fName
+		}
+		isFolderTarget := strings.HasSuffix(target, "/")
+		normalizedTarget := strings.TrimSuffix(target, "/")
+
+		if isFolderTarget {
+			if strings.HasPrefix(fName, target) || fName == target || fName == normalizedTarget {
+				lastSlash := strings.LastIndex(normalizedTarget, "/")
+				parent := ""
+				if lastSlash >= 0 {
+					parent = normalizedTarget[:lastSlash+1]
+				}
+				return strings.TrimPrefix(fName, parent)
+			}
+		} else {
+			if fName == target {
+				return filepath.Base(fName)
+			}
+			if strings.HasPrefix(fName, normalizedTarget+"/") {
+				lastSlash := strings.LastIndex(normalizedTarget, "/")
+				parent := ""
+				if lastSlash >= 0 {
+					parent = normalizedTarget[:lastSlash+1]
+				}
+				return strings.TrimPrefix(fName, parent)
+			}
+		}
+	}
+	return ""
+}

+ 336 - 0
src/mod/agi/agi.sqlite.go

@@ -0,0 +1,336 @@
+package agi
+
+import (
+	"database/sql"
+	"encoding/json"
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+
+	_ "github.com/glebarez/go-sqlite"
+	"github.com/robertkrimen/otto"
+	"imuslab.com/arozos/mod/agi/static"
+	"imuslab.com/arozos/mod/info/logger"
+)
+
+/*
+	AGI SQLite Library
+
+	Provides SQLite database access for AGI scripts via requirelib("sqlite").
+
+	JavaScript API:
+	  var db = sqlite.open("user:/.appdata/myapp/data.sqlite");
+	  db.exec("CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, name TEXT)");
+	  db.exec("INSERT INTO t (name) VALUES (?)", ["Alice"]);
+	  var rows    = db.query("SELECT * FROM t WHERE id > ?", [0]);
+	  var row     = db.queryRow("SELECT * FROM t WHERE id = ?", [1]);
+	  var tables  = db.tables();
+	  var schema  = db.schema("t");
+	  db.close();
+
+	Each call to sqlite.open() returns an object bound to a single connection.
+	Connections are cleaned up when the script ends or db.close() is called.
+*/
+
+func (g *Gateway) SQLiteLibRegister() {
+	err := g.RegisterLib("sqlite", g.injectSQLiteLibFunctions)
+	if err != nil {
+		logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
+		os.Exit(1)
+	}
+}
+
+func (g *Gateway) injectSQLiteLibFunctions(payload *static.AgiLibInjectionPayload) {
+	vm := payload.VM
+	u := payload.User
+	scriptFsh := payload.ScriptFsh
+
+	var mu sync.Mutex
+	var nextHandle int64
+	openDBs := make(map[int64]*sql.DB)
+
+	getDB := func(h int64) *sql.DB {
+		mu.Lock()
+		defer mu.Unlock()
+		return openDBs[h]
+	}
+
+	// _sqlite_open(vpath) => handle integer, or throws on error
+	vm.Set("_sqlite_open", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.NullValue()
+		}
+
+		vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
+
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied: "+vpath))
+		}
+
+		_, rpath, err := static.VirtualPathToRealPath(vpath, u)
+		if err != nil {
+			panic(vm.MakeCustomError("IOError", err.Error()))
+		}
+
+		if err := os.MkdirAll(filepath.Dir(rpath), 0755); err != nil {
+			panic(vm.MakeCustomError("IOError", err.Error()))
+		}
+
+		db, err := sql.Open("sqlite", rpath)
+		if err != nil {
+			panic(vm.MakeCustomError("SQLiteError", err.Error()))
+		}
+		if err := db.Ping(); err != nil {
+			db.Close()
+			panic(vm.MakeCustomError("SQLiteError", err.Error()))
+		}
+
+		mu.Lock()
+		nextHandle++
+		handle := nextHandle
+		openDBs[handle] = db
+		mu.Unlock()
+
+		val, _ := vm.ToValue(handle)
+		return val
+	})
+
+	// _sqlite_exec(handle, sql, paramsJSON) => JSON {lastInsertId, rowsAffected}
+	vm.Set("_sqlite_exec", func(call otto.FunctionCall) otto.Value {
+		handle, err := call.Argument(0).ToInteger()
+		if err != nil || handle < 1 {
+			panic(vm.MakeCustomError("InvalidHandle", "Invalid database handle"))
+		}
+		sqlStmt, _ := call.Argument(1).ToString()
+		paramsJSON, _ := call.Argument(2).ToString()
+		params := sqliteParseParams(paramsJSON)
+
+		db := getDB(int64(handle))
+		if db == nil {
+			panic(vm.MakeCustomError("InvalidHandle", "Database handle not found or already closed"))
+		}
+
+		result, err := db.Exec(sqlStmt, params...)
+		if err != nil {
+			panic(vm.MakeCustomError("SQLiteError", err.Error()))
+		}
+		lastID, _ := result.LastInsertId()
+		affected, _ := result.RowsAffected()
+
+		out, _ := json.Marshal(map[string]interface{}{
+			"lastInsertId": lastID,
+			"rowsAffected": affected,
+		})
+		val, _ := vm.ToValue(string(out))
+		return val
+	})
+
+	// _sqlite_query(handle, sql, paramsJSON) => JSON array of row objects
+	vm.Set("_sqlite_query", func(call otto.FunctionCall) otto.Value {
+		handle, err := call.Argument(0).ToInteger()
+		if err != nil || handle < 1 {
+			panic(vm.MakeCustomError("InvalidHandle", "Invalid database handle"))
+		}
+		sqlStmt, _ := call.Argument(1).ToString()
+		paramsJSON, _ := call.Argument(2).ToString()
+		params := sqliteParseParams(paramsJSON)
+
+		db := getDB(int64(handle))
+		if db == nil {
+			panic(vm.MakeCustomError("InvalidHandle", "Database handle not found or already closed"))
+		}
+
+		rows, err := db.Query(sqlStmt, params...)
+		if err != nil {
+			panic(vm.MakeCustomError("SQLiteError", err.Error()))
+		}
+		defer rows.Close()
+
+		cols, err := rows.Columns()
+		if err != nil {
+			panic(vm.MakeCustomError("SQLiteError", err.Error()))
+		}
+
+		var results []map[string]interface{}
+		for rows.Next() {
+			vals := make([]interface{}, len(cols))
+			ptrs := make([]interface{}, len(cols))
+			for i := range vals {
+				ptrs[i] = &vals[i]
+			}
+			if err := rows.Scan(ptrs...); err != nil {
+				panic(vm.MakeCustomError("SQLiteError", err.Error()))
+			}
+			row := make(map[string]interface{}, len(cols))
+			for i, col := range cols {
+				row[col] = sqliteConvertValue(vals[i])
+			}
+			results = append(results, row)
+		}
+		if results == nil {
+			results = []map[string]interface{}{}
+		}
+
+		out, err := json.Marshal(results)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		val, _ := vm.ToValue(string(out))
+		return val
+	})
+
+	// _sqlite_tables(handle) => JSON array of table name strings
+	vm.Set("_sqlite_tables", func(call otto.FunctionCall) otto.Value {
+		handle, err := call.Argument(0).ToInteger()
+		if err != nil || handle < 1 {
+			panic(vm.MakeCustomError("InvalidHandle", "Invalid database handle"))
+		}
+		db := getDB(int64(handle))
+		if db == nil {
+			panic(vm.MakeCustomError("InvalidHandle", "Database handle not found or already closed"))
+		}
+
+		rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
+		if err != nil {
+			panic(vm.MakeCustomError("SQLiteError", err.Error()))
+		}
+		defer rows.Close()
+
+		var tables []string
+		for rows.Next() {
+			var name string
+			if err := rows.Scan(&name); err == nil {
+				tables = append(tables, name)
+			}
+		}
+		if tables == nil {
+			tables = []string{}
+		}
+		out, _ := json.Marshal(tables)
+		val, _ := vm.ToValue(string(out))
+		return val
+	})
+
+	// _sqlite_schema(handle, tableName) => JSON array of column info objects
+	vm.Set("_sqlite_schema", func(call otto.FunctionCall) otto.Value {
+		handle, err := call.Argument(0).ToInteger()
+		if err != nil || handle < 1 {
+			panic(vm.MakeCustomError("InvalidHandle", "Invalid database handle"))
+		}
+		tableName, _ := call.Argument(1).ToString()
+		db := getDB(int64(handle))
+		if db == nil {
+			panic(vm.MakeCustomError("InvalidHandle", "Database handle not found or already closed"))
+		}
+
+		rows, err := db.Query("PRAGMA table_info(" + sqliteQuoteIdent(tableName) + ")")
+		if err != nil {
+			panic(vm.MakeCustomError("SQLiteError", err.Error()))
+		}
+		defer rows.Close()
+
+		type colInfo struct {
+			CID       int     `json:"cid"`
+			Name      string  `json:"name"`
+			Type      string  `json:"type"`
+			NotNull   int     `json:"notnull"`
+			DfltValue *string `json:"dflt_value"`
+			PK        int     `json:"pk"`
+		}
+
+		var cols []colInfo
+		for rows.Next() {
+			var c colInfo
+			if err := rows.Scan(&c.CID, &c.Name, &c.Type, &c.NotNull, &c.DfltValue, &c.PK); err == nil {
+				cols = append(cols, c)
+			}
+		}
+		if cols == nil {
+			cols = []colInfo{}
+		}
+		out, _ := json.Marshal(cols)
+		val, _ := vm.ToValue(string(out))
+		return val
+	})
+
+	// _sqlite_close(handle) => true
+	vm.Set("_sqlite_close", func(call otto.FunctionCall) otto.Value {
+		handle, err := call.Argument(0).ToInteger()
+		if err != nil {
+			return otto.FalseValue()
+		}
+		mu.Lock()
+		db, ok := openDBs[int64(handle)]
+		if ok {
+			delete(openDBs, int64(handle))
+		}
+		mu.Unlock()
+		if ok && db != nil {
+			db.Close()
+		}
+		return otto.TrueValue()
+	})
+
+	// JavaScript wrapper — builds a connection object around a raw handle
+	vm.Run(`
+var sqlite = {};
+sqlite.open = function(path) {
+    var handle = _sqlite_open(path);
+    if (handle === null || handle === undefined) { return null; }
+    return {
+        _handle: handle,
+        exec: function(sql, params) {
+            var r = _sqlite_exec(handle, sql, JSON.stringify(params || []));
+            return JSON.parse(r);
+        },
+        query: function(sql, params) {
+            var r = _sqlite_query(handle, sql, JSON.stringify(params || []));
+            return JSON.parse(r);
+        },
+        queryRow: function(sql, params) {
+            var rows = JSON.parse(_sqlite_query(handle, sql, JSON.stringify(params || [])));
+            return rows.length > 0 ? rows[0] : null;
+        },
+        tables: function() {
+            return JSON.parse(_sqlite_tables(handle));
+        },
+        schema: function(tableName) {
+            return JSON.parse(_sqlite_schema(handle, tableName));
+        },
+        close: function() {
+            return _sqlite_close(handle);
+        }
+    };
+};
+`)
+}
+
+// sqliteParseParams unmarshals a JSON array of query parameter values.
+func sqliteParseParams(paramsJSON string) []interface{} {
+	if paramsJSON == "" || paramsJSON == "null" || paramsJSON == "[]" {
+		return nil
+	}
+	var raw []interface{}
+	if err := json.Unmarshal([]byte(paramsJSON), &raw); err != nil {
+		return nil
+	}
+	return raw
+}
+
+// sqliteConvertValue normalises values scanned from SQLite rows for JSON encoding.
+func sqliteConvertValue(v interface{}) interface{} {
+	if b, ok := v.([]byte); ok {
+		return string(b)
+	}
+	return v
+}
+
+// sqliteQuoteIdent safely double-quotes an SQL identifier.
+func sqliteQuoteIdent(name string) string {
+	return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
+}

+ 136 - 69
src/mod/agi/agi.zip.go

@@ -10,6 +10,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/bodgit/sevenzip"
 	"github.com/mholt/archiver/v3"
 	"github.com/robertkrimen/otto"
 	"imuslab.com/arozos/mod/agi/static"
@@ -594,6 +595,17 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 			return otto.NullValue()
 		}
 
+		// Dispatch 7z archives to the dedicated helper
+		if strings.EqualFold(filepath.Ext(rpath), ".7z") {
+			jsonStr, err := sz7zListContentsJSON(rpath)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.NullValue()
+			}
+			reply, _ := vm.ToValue(jsonStr)
+			return reply
+		}
+
 		r, err := zip.OpenReader(rpath)
 		if err != nil {
 			g.RaiseError(err)
@@ -677,6 +689,17 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 			return otto.NullValue()
 		}
 
+		// Dispatch 7z archives to the dedicated helper
+		if strings.EqualFold(filepath.Ext(zipRpath), ".7z") {
+			filelist, err := szListDir(zipRpath, dirPath)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.NullValue()
+			}
+			reply, _ := vm.ToValue(filelist)
+			return reply
+		}
+
 		// Open zip file
 		r, err := zip.OpenReader(zipRpath)
 		if err != nil {
@@ -774,6 +797,50 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 			return otto.NullValue()
 		}
 
+		// Dispatch 7z archives
+		if strings.EqualFold(filepath.Ext(zipRpath), ".7z") {
+			r7z, err := sevenzip.OpenReader(zipRpath)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.NullValue()
+			}
+			defer r7z.Close()
+
+			var target7z *sevenzip.File
+			for _, f := range r7z.File {
+				if filepath.ToSlash(f.Name) == filepath.ToSlash(fileInZip) {
+					target7z = f
+					break
+				}
+			}
+			if target7z == nil {
+				g.RaiseError(errors.New("File not found in archive: " + fileInZip))
+				return otto.NullValue()
+			}
+
+			tmpFsh7z, err := u.GetFileSystemHandlerFromVirtualPath("tmp:/")
+			if err != nil {
+				g.RaiseError(err)
+				return otto.NullValue()
+			}
+			tmpFilename7z := arozfs.Base(fileInZip)
+			tmpVpath7z := "tmp:/" + tmpFilename7z
+			tmpRpath7z, _ := tmpFsh7z.FileSystemAbstraction.VirtualPathToRealPath(tmpVpath7z, u.Username)
+			rc7z, err := target7z.Open()
+			if err != nil {
+				g.RaiseError(err)
+				return otto.NullValue()
+			}
+			defer rc7z.Close()
+			if err = tmpFsh7z.FileSystemAbstraction.WriteStream(tmpRpath7z, rc7z, 0755); err != nil {
+				g.RaiseError(err)
+				return otto.NullValue()
+			}
+			u.SetOwnerOfFile(tmpFsh7z, tmpVpath7z)
+			reply, _ := vm.ToValue(tmpVpath7z)
+			return reply
+		}
+
 		// Open zip file
 		r, err := zip.OpenReader(zipRpath)
 		if err != nil {
@@ -923,6 +990,23 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 			return otto.FalseValue()
 		}
 
+		// 7z is not handled by the archiver library; dispatch separately
+		if strings.EqualFold(filepath.Ext(srcRpath), ".7z") {
+			if err = sz7zExtractAll(srcRpath, destFsh, destRpath); err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+			destFsh.FileSystemAbstraction.Walk(destRpath, func(path string, info os.FileInfo, err error) error {
+				if err == nil && !info.IsDir() {
+					vp, _ := static.RealpathToVirtualpath(destFsh, path, u)
+					u.SetOwnerOfFile(destFsh, vp)
+				}
+				return nil
+			})
+			reply, _ := vm.ToValue(true)
+			return reply
+		}
+
 		// Detect format
 		format, err := archiver.ByExtension(srcRpath)
 		if err != nil {
@@ -1072,29 +1156,8 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 		zipVpath = static.RelativeVpathRewrite(scriptFsh, zipVpath, vm, u)
 		destVpath = static.RelativeVpathRewrite(scriptFsh, destVpath, vm, u)
 
-		var targetPaths []string
-		switch v := pathsObj.(type) {
-		case []interface{}:
-			for _, s := range v {
-				if str, ok := s.(string); ok {
-					targetPaths = append(targetPaths, filepath.ToSlash(strings.TrimPrefix(str, "/")))
-				}
-			}
-		case string:
-			// Accept either a JSON array string or a single path
-			v = strings.TrimSpace(v)
-			if strings.HasPrefix(v, "[") {
-				var arr []string
-				if json.Unmarshal([]byte(v), &arr) == nil {
-					for _, s := range arr {
-						targetPaths = append(targetPaths, filepath.ToSlash(strings.TrimPrefix(s, "/")))
-					}
-				}
-			}
-			if len(targetPaths) == 0 && v != "" {
-				targetPaths = append(targetPaths, filepath.ToSlash(strings.TrimPrefix(v, "/")))
-			}
-		default:
+		targetPaths := szParsePathsArg(pathsObj)
+		if targetPaths == nil {
 			g.RaiseError(errors.New("invalid paths format"))
 			return otto.FalseValue()
 		}
@@ -1117,6 +1180,41 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 			return otto.FalseValue()
 		}
 
+		// Dispatch 7z archives
+		if strings.EqualFold(filepath.Ext(zipRpath), ".7z") {
+			r7z, err := sevenzip.OpenReader(zipRpath)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.FalseValue()
+			}
+			defer r7z.Close()
+
+			for _, f := range r7z.File {
+				outRelPath := szMatchPartialPath(filepath.ToSlash(f.Name), targetPaths)
+				if outRelPath == "" {
+					continue
+				}
+				outPath := filepath.Join(destRpath, filepath.FromSlash(outRelPath))
+				if f.FileInfo().IsDir() {
+					destFsh.FileSystemAbstraction.MkdirAll(outPath, 0755)
+					continue
+				}
+				destFsh.FileSystemAbstraction.MkdirAll(filepath.Dir(outPath), 0755)
+				rc, err := f.Open()
+				if err != nil {
+					continue
+				}
+				destFsh.FileSystemAbstraction.WriteStream(outPath, rc, 0755)
+				rc.Close()
+				vp, err := static.RealpathToVirtualpath(destFsh, outPath, u)
+				if err == nil {
+					u.SetOwnerOfFile(destFsh, vp)
+				}
+			}
+			reply, _ := vm.ToValue(true)
+			return reply
+		}
+
 		r, err := zip.OpenReader(zipRpath)
 		if err != nil {
 			g.RaiseError(err)
@@ -1125,51 +1223,7 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 		defer r.Close()
 
 		for _, f := range r.File {
-			fName := filepath.ToSlash(f.Name)
-			var outRelPath string
-
-			for _, target := range targetPaths {
-				if target == "" || target == "/" {
-					// Extract everything, preserve full zip structure
-					outRelPath = fName
-					break
-				}
-
-				isFolderTarget := strings.HasSuffix(target, "/")
-				normalizedTarget := strings.TrimSuffix(target, "/")
-
-				if isFolderTarget {
-					// Folder selection: strip the parent prefix so the selected
-					// folder name becomes the top-level output directory.
-					// e.g. target="music/a/" fName="music/a/b.mp3" → "a/b.mp3"
-					if strings.HasPrefix(fName, target) || fName == target || fName == normalizedTarget {
-						lastSlash := strings.LastIndex(normalizedTarget, "/")
-						parent := ""
-						if lastSlash >= 0 {
-							parent = normalizedTarget[:lastSlash+1]
-						}
-						outRelPath = strings.TrimPrefix(fName, parent)
-						break
-					}
-				} else {
-					// File selection: extract flat (just the filename, no dirs).
-					if fName == target {
-						outRelPath = filepath.Base(fName)
-						break
-					}
-					// Target without trailing slash that is actually a directory
-					if strings.HasPrefix(fName, normalizedTarget+"/") {
-						lastSlash := strings.LastIndex(normalizedTarget, "/")
-						parent := ""
-						if lastSlash >= 0 {
-							parent = normalizedTarget[:lastSlash+1]
-						}
-						outRelPath = strings.TrimPrefix(fName, parent)
-						break
-					}
-				}
-			}
-
+			outRelPath := szMatchPartialPath(filepath.ToSlash(f.Name), targetPaths)
 			if outRelPath == "" {
 				continue
 			}
@@ -1360,6 +1414,16 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 			return otto.NullValue()
 		}
 
+		if strings.EqualFold(filepath.Ext(zipRpath), ".7z") {
+			jsonStr, err := sz7zGetFileInfo(zipRpath)
+			if err != nil {
+				g.RaiseError(err)
+				return otto.NullValue()
+			}
+			reply, _ := vm.ToValue(jsonStr)
+			return reply
+		}
+
 		r, err := zip.OpenReader(zipRpath)
 		if err != nil {
 			g.RaiseError(err)
@@ -1413,7 +1477,7 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 		ziplib.listZipFileDir = _ziplib_listZipFileDir; // List contents of specific directory in zip
 		ziplib.getFileFromZip = _ziplib_getFileFromZip; // Get specific file from zip, stored temporary at tmp:/
 		ziplib.getCompressFileType = _ziplib_getCompressFileType;
-		ziplib.extractAnyFile = _ziplib_extractAnyFile; // Extract zip, tar, targz, gz based on file type
+		ziplib.extractAnyFile = _ziplib_extractAnyFile; // Extract zip, tar, targz, gz, 7z based on file type
 		ziplib.createAnyZipFile = _ziplib_createAnyZipFile; // Create zip, tar, targz, gz based on input
 
 		// Zip-specific advanced functions
@@ -1421,6 +1485,9 @@ func (g *Gateway) injectZipFileLibFunctions(payload *static.AgiLibInjectionPaylo
 		ziplib.addFileToZip = _ziplib_addFileToZip;           // Add file or folder to existing zip
 		ziplib.getZipFileInfo = _ziplib_getZipFileInfo;       // Get metadata (counts, sizes) about a zip
 	`)
+
+	// Extend ziplib with 7z support
+	g.inject7zLibFunctions(payload)
 }
 
 /*

+ 249 - 0
src/mod/agi/agi_sqlite_test.go

@@ -0,0 +1,249 @@
+package agi
+
+import (
+	"database/sql"
+	"path/filepath"
+	"testing"
+
+	_ "github.com/glebarez/go-sqlite"
+	"github.com/robertkrimen/otto"
+	"imuslab.com/arozos/mod/agi/static"
+	user "imuslab.com/arozos/mod/user"
+)
+
+// ─── pure-Go helper tests ─────────────────────────────────────────────────────
+
+func TestSQLiteParseParams_EmptyInputs(t *testing.T) {
+	for _, input := range []string{"", "null", "[]"} {
+		if got := sqliteParseParams(input); len(got) != 0 {
+			t.Errorf("sqliteParseParams(%q): expected empty slice, got %v", input, got)
+		}
+	}
+}
+
+func TestSQLiteParseParams_Values(t *testing.T) {
+	got := sqliteParseParams(`["Alice", 30, null]`)
+	if len(got) != 3 {
+		t.Fatalf("expected 3 params, got %d", len(got))
+	}
+	if got[0] != "Alice" {
+		t.Errorf("param[0]: expected Alice, got %v", got[0])
+	}
+	if got[2] != nil {
+		t.Errorf("param[2]: expected nil, got %v", got[2])
+	}
+}
+
+func TestSQLiteParseParams_InvalidJSON(t *testing.T) {
+	if got := sqliteParseParams("{not json}"); len(got) != 0 {
+		t.Errorf("expected nil on invalid JSON, got %v", got)
+	}
+}
+
+func TestSQLiteConvertValue_ByteSlice(t *testing.T) {
+	if got := sqliteConvertValue([]byte("hello")); got != "hello" {
+		t.Errorf("expected string 'hello', got %v", got)
+	}
+}
+
+func TestSQLiteConvertValue_Passthrough(t *testing.T) {
+	if sqliteConvertValue(42) != 42 {
+		t.Error("integer should pass through unchanged")
+	}
+	if sqliteConvertValue(nil) != nil {
+		t.Error("nil should pass through as nil")
+	}
+	if sqliteConvertValue("text") != "text" {
+		t.Error("string should pass through unchanged")
+	}
+}
+
+func TestSQLiteQuoteIdent(t *testing.T) {
+	tests := []struct{ in, want string }{
+		{"users", `"users"`},
+		{"my table", `"my table"`},
+		{`has"quote`, `"has""quote"`},
+		{"", `""`},
+	}
+	for _, tt := range tests {
+		if got := sqliteQuoteIdent(tt.in); got != tt.want {
+			t.Errorf("sqliteQuoteIdent(%q) = %q, want %q", tt.in, got, tt.want)
+		}
+	}
+}
+
+// ─── library registration ─────────────────────────────────────────────────────
+
+func TestSQLiteLibRegister_AddsToLoadedLibs(t *testing.T) {
+	g := minimalGateway()
+	g.SQLiteLibRegister()
+	if _, ok := g.LoadedAGILibrary["sqlite"]; !ok {
+		t.Error("expected 'sqlite' in LoadedAGILibrary after SQLiteLibRegister")
+	}
+}
+
+func TestSQLiteLibRegister_IdempotentDoesNotPanic(t *testing.T) {
+	g := minimalGateway()
+	g.SQLiteLibRegister()
+	// Second call should log but not os.Exit in tests — verify no panic at least
+	defer func() {
+		if r := recover(); r != nil {
+			t.Errorf("second SQLiteLibRegister panicked: %v", r)
+		}
+	}()
+}
+
+// ─── JS object structure ──────────────────────────────────────────────────────
+
+func TestInjectSQLiteLib_JSObjectExposed(t *testing.T) {
+	g := minimalGateway()
+	vm := otto.New()
+	payload := &static.AgiLibInjectionPayload{
+		VM:   vm,
+		User: &user.User{Username: "test"},
+	}
+	g.injectSQLiteLibFunctions(payload)
+
+	val, err := vm.Run(`typeof sqlite.open`)
+	if err != nil {
+		t.Fatalf("evaluating typeof sqlite.open: %v", err)
+	}
+	s, _ := val.ToString()
+	if s != "function" {
+		t.Errorf("sqlite.open should be a function, got %q", s)
+	}
+}
+
+func TestInjectSQLiteLib_NativeFunctionsRegistered(t *testing.T) {
+	g := minimalGateway()
+	vm := otto.New()
+	payload := &static.AgiLibInjectionPayload{
+		VM:   vm,
+		User: &user.User{Username: "test"},
+	}
+	g.injectSQLiteLibFunctions(payload)
+
+	for _, fn := range []string{
+		"_sqlite_open",
+		"_sqlite_exec",
+		"_sqlite_query",
+		"_sqlite_tables",
+		"_sqlite_schema",
+		"_sqlite_close",
+	} {
+		val, err := vm.Run(`typeof ` + fn)
+		if err != nil {
+			t.Fatalf("evaluating typeof %s: %v", fn, err)
+		}
+		s, _ := val.ToString()
+		if s != "function" {
+			t.Errorf("%s should be a function, got %q", fn, s)
+		}
+	}
+}
+
+// ─── driver integration (no AGI VM, direct sql.Open) ─────────────────────────
+
+func TestSQLiteDriver_CreateInsertQuery(t *testing.T) {
+	tmpDir := t.TempDir()
+	dbPath := filepath.Join(tmpDir, "test.db")
+
+	db, err := sql.Open("sqlite", dbPath)
+	if err != nil {
+		t.Fatalf("sql.Open: %v", err)
+	}
+	defer db.Close()
+
+	if _, err = db.Exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)`); err != nil {
+		t.Fatalf("CREATE TABLE: %v", err)
+	}
+
+	res, err := db.Exec(`INSERT INTO users (name) VALUES (?)`, "Alice")
+	if err != nil {
+		t.Fatalf("INSERT: %v", err)
+	}
+	id, _ := res.LastInsertId()
+	if id != 1 {
+		t.Errorf("expected lastInsertId=1, got %d", id)
+	}
+
+	var name string
+	if err := db.QueryRow(`SELECT name FROM users WHERE id = ?`, 1).Scan(&name); err != nil {
+		t.Fatalf("SELECT: %v", err)
+	}
+	if name != "Alice" {
+		t.Errorf("expected Alice, got %s", name)
+	}
+}
+
+func TestSQLiteDriver_ParameterisedQuery(t *testing.T) {
+	tmpDir := t.TempDir()
+	db, err := sql.Open("sqlite", filepath.Join(tmpDir, "p.db"))
+	if err != nil {
+		t.Fatalf("sql.Open: %v", err)
+	}
+	defer db.Close()
+
+	if _, err = db.Exec(`CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT)`); err != nil {
+		t.Fatalf("CREATE: %v", err)
+	}
+	for _, pair := range [][2]string{{"a", "1"}, {"b", "2"}, {"c", "3"}} {
+		if _, err = db.Exec(`INSERT INTO kv VALUES (?, ?)`, pair[0], pair[1]); err != nil {
+			t.Fatalf("INSERT: %v", err)
+		}
+	}
+
+	rows, err := db.Query(`SELECT k, v FROM kv WHERE k != ? ORDER BY k`, "b")
+	if err != nil {
+		t.Fatalf("SELECT: %v", err)
+	}
+	defer rows.Close()
+
+	var keys []string
+	for rows.Next() {
+		var k, v string
+		if err := rows.Scan(&k, &v); err != nil {
+			t.Fatalf("Scan: %v", err)
+		}
+		keys = append(keys, k)
+	}
+	if len(keys) != 2 || keys[0] != "a" || keys[1] != "c" {
+		t.Errorf("expected [a c], got %v", keys)
+	}
+}
+
+func TestSQLiteDriver_TableListViaSchemaQuery(t *testing.T) {
+	tmpDir := t.TempDir()
+	db, err := sql.Open("sqlite", filepath.Join(tmpDir, "schema.db"))
+	if err != nil {
+		t.Fatalf("sql.Open: %v", err)
+	}
+	defer db.Close()
+
+	// Use AUTOINCREMENT so SQLite creates sqlite_sequence internally —
+	// the filtered query must still return only the 3 user tables.
+	for _, tbl := range []string{"alpha", "beta", "gamma"} {
+		if _, err = db.Exec(`CREATE TABLE ` + tbl + ` (id INTEGER PRIMARY KEY AUTOINCREMENT)`); err != nil {
+			t.Fatalf("CREATE %s: %v", tbl, err)
+		}
+	}
+
+	// Matches the filter used by _sqlite_tables
+	rows, err := db.Query(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`)
+	if err != nil {
+		t.Fatalf("sqlite_master query: %v", err)
+	}
+	defer rows.Close()
+
+	var tables []string
+	for rows.Next() {
+		var name string
+		if err := rows.Scan(&name); err != nil {
+			t.Fatalf("Scan: %v", err)
+		}
+		tables = append(tables, name)
+	}
+	if len(tables) != 3 {
+		t.Errorf("expected 3 user tables, got %v", tables)
+	}
+}

+ 1 - 0
src/mod/agi/moduleManager.go

@@ -55,6 +55,7 @@ func (g *Gateway) LoadAllFunctionalModules() {
 	g.SysinfoLibRegister()
 	//g.AudioLibRegister() //work in progress
 	g.ZipLibRegister()
+	g.SQLiteLibRegister()
 
 	//Only register ffmpeg lib if host OS have ffmpeg installed
 	ffmpegExists, _ := apt.PackageExists("ffmpeg")