toolnet-memory 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,299 @@
1
- import{existsSync as y}from"node:fs";import{homedir as h}from"node:os";import{join as x}from"node:path";import{spawnSync as ee}from"node:child_process";import{existsSync as R,mkdirSync as $,readFileSync as M,writeFileSync as P}from"node:fs";import{homedir as _}from"node:os";import{dirname as J,join as B}from"node:path";function H(o){return`'${o.replace(/'/g,"'\\''")}'`}function S(o={}){let t=o.hooksFile??B(_(),".gemini","config","hooks.json");$(J(t),{recursive:!0});let e={};if(R(t))try{e=JSON.parse(M(t,"utf8"))}catch(a){throw new Error(`Invalid existing Agy hooks.json: ${a instanceof Error?a.message:String(a)}`)}let r=o.binary??"toolnet-memory",n=`${H(r)} session:agy-hook`;return e["toolnet-memory"]={enabled:!0,PreInvocation:[{type:"command",command:`${n} pre`,timeout:15}],PostInvocation:[{type:"command",command:`${n} post`,timeout:15}],Stop:[{type:"command",command:`${n} stop`,timeout:30}]},P(t,JSON.stringify(e,null,2)+`
2
- `,{encoding:"utf8",mode:384}),t}import{existsSync as D,mkdirSync as b,readFileSync as L,writeFileSync as C}from"node:fs";import{homedir as p}from"node:os";import{join as g}from"node:path";var k="<!-- TOOLNET_MEMORY_BOOTSTRAP_START -->",f="<!-- TOOLNET_MEMORY_BOOTSTRAP_END -->";function W(){let o=g(p(),".config","opencode","AGENTS.md");b(g(p(),".config","opencode"),{recursive:!0});let t=`${k}
3
- ## ToolNet Memory \u2014 Project Continuity
1
+ import{existsSync as je}from"node:fs";import{homedir as Ae}from"node:os";import{join as Me}from"node:path";import{spawnSync as Fe}from"node:child_process";import{homedir as Te}from"node:os";import{join as p}from"node:path";function _(e={}){return p(e.home??Te(),".gemini")}function I(e={}){return p(_(e),"config")}function x(e={}){return p(I(e),"mcp_config.json")}function C(e={}){return p(I(e),"hooks.json")}function P(e={}){return p(_(e),"antigravity-cli")}function J(e="toolnet-memory",n={}){return p(P(n),"plugins",e)}function D(e={}){return[P(e),I(e)]}import{homedir as Ee}from"node:os";import{join as y}from"node:path";function m(e={}){let n=e.xdgConfigHome??process.env.XDG_CONFIG_HOME?.trim();return n?y(n,"opencode"):y(e.home??Ee(),".config","opencode")}function $(e={}){return y(m(e),"opencode.json")}function L(e={}){return y(m(e),"plugins")}function H(e={}){return y(m(e),"AGENTS.md")}import{homedir as B}from"node:os";import{join as k}from"node:path";function N(e={}){return k(e.home??B(),".claude")}function U(e={}){return k(N(e),"settings.json")}function Y(e={}){return k(e.home??B(),".claude.json")}function Re(e){return Fe("sh",["-lc",`command -v ${JSON.stringify(e)} >/dev/null 2>&1`],{stdio:"ignore"}).status===0}function S(e){let n=e.commandExists(e.command),t=e.configPaths.filter(s=>je(s)),o=t.length>0,i=[];n&&i.push(`command:${e.command}`);for(let s of t)i.push(`config:${s}`);return{agent:e.agent,detected:n||o,commandDetected:n,configDetected:o,evidence:i}}function q(e={}){let n=e.home??Ae(),t=e.commandExists??Re,o=e.codexHome??process.env.CODEX_HOME??Me(n,".codex");return[S({agent:"agy",command:"agy",commandExists:t,configPaths:D({home:n})}),S({agent:"opencode",command:"opencode",commandExists:t,configPaths:[m({home:n,xdgConfigHome:e.xdgConfigHome})]}),S({agent:"claude",command:"claude",commandExists:t,configPaths:[N({home:n})]}),S({agent:"codex",command:"codex",commandExists:t,configPaths:[o]})]}import{existsSync as w,mkdirSync as z,readFileSync as Q,renameSync as Ve,writeFileSync as ze}from"node:fs";import{dirname as Qe,join as b}from"node:path";import{existsSync as _e,mkdirSync as Pe,readFileSync as Je,renameSync as De,rmSync as $e,writeFileSync as Le}from"node:fs";import{dirname as He}from"node:path";function Be(e){return`'${e.replace(/'/g,"'\\''")}'`}function W(e={}){let n=e.hooksFile??C();Pe(He(n),{recursive:!0,mode:448});let t={};if(_e(n)){let r;try{r=JSON.parse(Je(n,"utf8"))}catch(c){throw new Error(`Invalid existing Agy hooks.json: ${c instanceof Error?c.message:String(c)}`)}if(typeof r!="object"||r===null||Array.isArray(r))throw new Error("Invalid existing Agy hooks.json: root must be a JSON object.");t=r}let o=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",i=`${Be(o)} session:agy-hook`;t["toolnet-memory"]={enabled:!0,PreToolUse:[{matcher:"view_file|list_dir|find_by_name|grep_search|run_command",hooks:[{type:"command",command:`${i} pre-tool`,timeout:5}]}],PreInvocation:[{type:"command",command:`${i} pre`,timeout:15}],PostInvocation:[{type:"command",command:`${i} post`,timeout:15}],Stop:[{type:"command",command:`${i} stop`,timeout:30}]};let s=`${n}.tmp-${process.pid}-${Date.now()}`;try{Le(s,JSON.stringify(t,null,2)+`
2
+ `,{encoding:"utf8",mode:384}),De(s,n)}finally{$e(s,{force:!0})}return n}import{existsSync as Ue,mkdirSync as Ye,readFileSync as qe,renameSync as We,writeFileSync as Ge}from"node:fs";import{dirname as Xe}from"node:path";function h(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Ke(e,n){Ye(Xe(e),{recursive:!0,mode:448});let t=`${e}.tmp-${process.pid}-${Date.now()}`;Ge(t,`${JSON.stringify(n,null,2)}
3
+ `,{encoding:"utf8",mode:384}),We(t,e)}function G(e){if(!Ue(e))return{};let n=qe(e,"utf8").trim();if(!n)return{};let t;try{t=JSON.parse(n)}catch(o){throw new Error(`Invalid existing Agy MCP config: ${o instanceof Error?o.message:String(o)}`)}if(!h(t))throw new Error("Invalid existing Agy MCP config: root must be a JSON object.");return t}function X(e,n){return h(e)?e.command===n&&Array.isArray(e.args)&&e.args.length===1&&e.args[0]==="mcp":!1}function K(e={}){let n=e.configFile??x(),t=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",o=e.serverName??"toolnet-memory",i=G(n),s=i.mcpServers;if(s!==void 0&&!h(s))throw new Error("Invalid existing Agy MCP config: mcpServers must be an object.");let r=h(s)?{...s}:{},c=r[o];if(X(c,t))return{installed:!0,changed:!1,configFile:n,serverName:o,command:t,args:["mcp"]};r[o]={command:t,args:["mcp"]};let l={...i,mcpServers:r};Ke(n,l);let a=G(n).mcpServers;if(!h(a)||!X(a[o],t))throw new Error("Agy MCP configuration was written but verification failed.");return{installed:!0,changed:!0,configFile:n,serverName:o,command:t,args:["mcp"]}}var Ze=`# ToolNet Memory Continuity
4
4
 
5
- When the current workspace belongs to a ToolNet project, identified by a nearest ancestor containing \`.toolnet/project.json\`:
5
+ ToolNet Memory is the authoritative continuity layer for previous project work.
6
6
 
7
- 1. Before planning, editing, or running implementation commands, read that project root's \`.toolnet/context/startup.md\`.
8
- 2. Treat its PROJECT RULES, MISSION, CURRENT OBJECTIVE, rationale, current phase/task, Definition of Done, blockers and next actions as project continuity context.
9
- 3. Do not restart work already marked completed unless repository evidence proves it must be revisited.
10
- 4. Verify current repository state before continuing old work.
11
- 5. If \`.toolnet/context/startup.md\` is missing, run \`toolnet-memory context:sync --project <project-root>\`, then read it.
12
- 6. Never invent missing rationale. If ToolNet says a reason was not explicitly recorded, inspect the implementation or ask rather than guessing.
7
+ ## Resume / continue behavior
13
8
 
14
- This applies automatically to every ToolNet-managed project.
15
- ${f}`,e=D(o)?L(o,"utf8"):"",r=e.indexOf(k),n=e.indexOf(f);return r>=0&&n>=r?e=e.slice(0,r)+t+e.slice(n+f.length):(e=e.trimEnd(),e&&(e+=`
9
+ Whenever the user asks to continue, resume, finish, pick up, return to, or complete previous work:
16
10
 
17
- `),e+=t),C(o,e.trimEnd()+`
18
- `,{encoding:"utf8",mode:384}),o}function I(o={}){let t=o.directory??g(p(),".config","opencode","plugins");b(t,{recursive:!0}),W();let e=g(t,"toolnet-memory.js"),r=o.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",n=`// Generated by ToolNet Memory.
19
- // OpenCode session capture + continuity bootstrap.
11
+ 1. FIRST call the ToolNet Memory MCP tool \`memory_agent_ask\`.
12
+ 2. Use ToolNet's compact continuity result to determine:
13
+ - current task
14
+ - completed work
15
+ - current or last file
16
+ - TODOs
17
+ - blockers
18
+ - next action
19
+ 3. Only AFTER continuity is known may you inspect current source or git to verify repository truth.
20
20
 
21
- function getSessionId(event) {
22
- const p = event?.properties ?? {}
21
+ ## Forbidden continuity recovery
22
+
23
+ Do NOT reconstruct previous work by reading, listing, searching, or shelling into:
24
+
25
+ - \`.toolnet/sessions/**\`
26
+ - \`state.json\`
27
+ - \`events.jsonl\`
28
+ - raw transcripts
29
+ - \`~/.gemini/antigravity-cli/brain/**\`
30
+ - Antigravity \`transcript.jsonl\`
31
+ - another coding agent's internal session history
32
+
33
+ Do NOT run Bash/cat/tail/grep against those locations to discover previous work.
34
+
35
+ Do NOT search the filesystem for the implementation or schema of \`memory_agent_ask\`.
36
+ Invoke the MCP tool directly.
37
+
38
+ For direct continuity facts, prefer \`mode="local"\`.
39
+ Use \`mode="ai"\` only when continuity is ambiguous or requires synthesis.
40
+
41
+ Current repository evidence overrides stale memory after ToolNet has restored the working context.
42
+
43
+ Do not ask the user to repeat context already available through ToolNet Memory.
44
+ `;function Z(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function T(e,n){z(Qe(e),{recursive:!0});let t=`${e}.tmp-${process.pid}-${Date.now()}`;ze(t,n,{encoding:"utf8",mode:384}),Ve(t,e)}function V(e,n){w(e)&&Q(e,"utf8")===n||T(e,n)}function ee(e){if(!w(e))return{};let n=Q(e,"utf8").trim();if(!n)return{};let t;try{t=JSON.parse(n)}catch(o){throw new Error(`Invalid legacy Antigravity config ${e}: ${o instanceof Error?o.message:String(o)}`)}if(!Z(t))throw new Error(`Invalid legacy Antigravity config ${e}: root must be object`);return t}function et(e,n){if(!w(e))return!1;let t=ee(e);if(!Z(t.mcpServers)||!Object.prototype.hasOwnProperty.call(t.mcpServers,n))return!1;let o={...t.mcpServers};return delete o[n],T(e,`${JSON.stringify({...t,mcpServers:o},null,2)}
45
+ `),!0}function tt(e){if(!w(e))return!1;let n=ee(e);if(!Object.prototype.hasOwnProperty.call(n,"toolnet-memory"))return!1;let t={...n};return delete t["toolnet-memory"],T(e,`${JSON.stringify(t,null,2)}
46
+ `),!0}function te(e={}){let n=e.pluginName??"toolnet-memory",t=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",o=e.pluginRoot??J(n),i=b(o,"plugin.json"),s=b(o,"mcp_config.json"),r=b(o,"hooks.json"),c=b(o,"rules","toolnet-memory-continuity.md");z(o,{recursive:!0,mode:448}),V(i,`${JSON.stringify({$schema:"https://antigravity.google/schemas/v1/plugin.json",name:n,description:"Persistent project continuity and memory for Antigravity coding sessions."},null,2)}
47
+ `),K({configFile:s,binary:t,serverName:"toolnet-memory"}),W({hooksFile:r,binary:t}),V(c,`${Ze.trim()}
48
+ `);let l=e.legacyMcpFile??x(),u=e.legacyHooksFile??C(),a=[];return l!==s&&et(l,"toolnet-memory")&&a.push(l),u!==r&&tt(u)&&a.push(u),{installed:!0,pluginRoot:o,files:[i,s,r,c],migratedLegacy:a}}import{existsSync as ot,mkdirSync as re,readFileSync as rt,writeFileSync as se}from"node:fs";import{join as st}from"node:path";var nt="memory_agent_ask";function ne(){return`
49
+ [TOOLNET MEMORY AGENT]
50
+
51
+ Tool available:
52
+ - ${nt}
53
+
54
+ Use it automatically BEFORE guessing when:
55
+
56
+ - The user asks to continue, resume, or pick up previous work.
57
+ - The user says things like:
58
+ - "ti\u1EBFp t\u1EE5c task l\xFAc n\xE3y"
59
+ - "l\xE0m ti\u1EBFp ph\u1EA7n \u0111ang d\u1EDF"
60
+ - "agent tr\u01B0\u1EDBc \u0111ang l\xE0m g\xEC?"
61
+ - "d\u1EEBng \u1EDF \u0111\xE2u?"
62
+ - "todo n\xE0o ch\u01B0a xong?"
63
+ - "continue the previous task"
64
+ - "resume the last session"
65
+ - Previous-agent state, unfinished work, blockers,
66
+ decisions, touched files, or next actions are unclear.
67
+ - Fast startup context is not enough to safely continue.
68
+
69
+ Preferred mode:
70
+
71
+ - mode="local"
72
+ for direct factual questions such as:
73
+ current task, last file, blocker, completed TODOs.
74
+
75
+ - mode="ai"
76
+ for composite or ambiguous continuity questions
77
+ that benefit from ToolNet Memory Agent reasoning.
78
+
79
+ Do NOT call it automatically when:
80
+
81
+ - Normal startup context already gives enough information.
82
+ - The question is unrelated to previous project work.
83
+ - The answer is obvious from current repository evidence.
84
+
85
+ Rules:
86
+
87
+ - Never invent previous work.
88
+ - Current repository evidence overrides stale memory.
89
+ - NEVER reconstruct previous work by reading ToolNet internal session files.
90
+ - NEVER read/list/search .toolnet/sessions/**, session state.json,
91
+ events.jsonl, or raw transcripts to discover previous-agent state.
92
+ - Do not search the filesystem for the implementation/schema of
93
+ memory_agent_ask. Invoke the MCP tool directly when deeper
94
+ continuity is required.
95
+ - Do not dump raw transcripts or full memory.
96
+ - After receiving the ToolNet answer, continue the task
97
+ instead of asking the user to repeat known context.
98
+ - If ToolNet says information is not recorded, say so.
99
+ `.trim()}var oe="<!-- TOOLNET_MEMORY_BOOTSTRAP_START -->",E="<!-- TOOLNET_MEMORY_BOOTSTRAP_END -->";function it(){let e=H();re(m(),{recursive:!0});let n=`${oe}
100
+ ## ToolNet Memory
101
+
102
+ ToolNet Memory is available for projects that already contain a valid \`.toolnet/project.json\`.
103
+
104
+ Rules:
105
+
106
+ 1. Do not create a ToolNet project automatically.
107
+ 2. If no valid \`.toolnet/project.json\` exists in the current project or an ancestor, ignore ToolNet Memory.
108
+ 3. Normal startup context must stay small and selective.
109
+ 4. Do not automatically run deep recovery commands.
110
+ 5. Use ToolNet MCP or selective retrieval when older project knowledge is actually needed.
111
+ 6. Raw transcripts must not be injected into prompts.
112
+ 7. Current repository evidence has priority over stale memory when they conflict.
113
+
114
+
115
+ ${ne()}
116
+
117
+ ${E}`,t=ot(e)?rt(e,"utf8"):"",o=t.indexOf(oe),i=t.indexOf(E);return o>=0&&i>=o?t=t.slice(0,o)+n+t.slice(i+E.length):(t=t.trimEnd(),t&&(t+=`
118
+
119
+ `),t+=n),se(e,t.trimEnd()+`
120
+ `,{encoding:"utf8",mode:384}),e}function ie(e={}){let n=e.directory??L();re(n,{recursive:!0}),it();let t=st(n,"toolnet-memory.js"),o=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",i=`
121
+ // Generated by ToolNet Memory.
122
+ // OpenCode integration:
123
+ // - project gated
124
+ // - selective context injection
125
+ // - incremental session checkpoints
126
+ // - periodic durability sync
127
+ // - best-effort shutdown flush
128
+
129
+ import fs from "node:fs"
130
+ import path from "node:path"
131
+ import os from "node:os"
132
+
133
+ const TOOLNET_BINARY =
134
+ ${JSON.stringify(o)}
135
+
136
+ const CONTEXT_MAX_TOKENS = 700
137
+
138
+ const CONTEXT_CACHE_MS = 5000
139
+
140
+ const LOCAL_CAPTURE_MS = 15000
141
+
142
+ const REMOTE_SYNC_MS = 60000
143
+
144
+ const EVENT_CAPTURE_DEBOUNCE_MS = 1200
145
+
146
+ const CAPTURE_TIMEOUT_MS = 20000
147
+
148
+ const REMOTE_TIMEOUT_MS = 120000
149
+
150
+ const GLOBAL_STATUS_FILE = path.join(
151
+ os.homedir(),
152
+ ".config",
153
+ "toolnet-memory",
154
+ "opencode-sync-status.json"
155
+ )
156
+
157
+ function projectStatusFile(
158
+ data
159
+ ) {
160
+ if (
161
+ typeof data?.projectRoot ===
162
+ "string" &&
163
+ data.projectRoot.length > 0
164
+ ) {
165
+ return path.join(
166
+ data.projectRoot,
167
+ ".toolnet",
168
+ "runtime",
169
+ "opencode-status.json"
170
+ )
171
+ }
172
+
173
+ return GLOBAL_STATUS_FILE
174
+ }
175
+
176
+ function writeStatus(data) {
177
+ try {
178
+ const statusFile =
179
+ projectStatusFile(
180
+ data
181
+ )
182
+
183
+ fs.mkdirSync(
184
+ path.dirname(
185
+ statusFile
186
+ ),
187
+ {
188
+ recursive: true,
189
+ }
190
+ )
191
+
192
+ fs.writeFileSync(
193
+ statusFile,
194
+ JSON.stringify(
195
+ {
196
+ timestamp:
197
+ new Date()
198
+ .toISOString(),
199
+ ...data,
200
+ },
201
+ null,
202
+ 2
203
+ ) + "\\n"
204
+ )
205
+ } catch {
206
+ // Status reporting must
207
+ // never break OpenCode.
208
+ }
209
+ }
210
+
211
+ function validProjectManifest(
212
+ file
213
+ ) {
214
+ try {
215
+ const parsed =
216
+ JSON.parse(
217
+ fs.readFileSync(
218
+ file,
219
+ "utf8"
220
+ )
221
+ )
222
+
223
+ return Boolean(
224
+ parsed &&
225
+ typeof parsed ===
226
+ "object" &&
227
+ typeof parsed.id ===
228
+ "string" &&
229
+ parsed.id.length > 0
230
+ )
231
+ } catch {
232
+ return false
233
+ }
234
+ }
235
+
236
+ function findProjectRoot(
237
+ inputDirectory
238
+ ) {
239
+ let current =
240
+ path.resolve(
241
+ inputDirectory
242
+ )
243
+
244
+ const filesystemRoot =
245
+ path.parse(
246
+ current
247
+ ).root
248
+
249
+ while (true) {
250
+ const manifest =
251
+ path.join(
252
+ current,
253
+ ".toolnet",
254
+ "project.json"
255
+ )
256
+
257
+ if (
258
+ fs.existsSync(
259
+ manifest
260
+ ) &&
261
+ validProjectManifest(
262
+ manifest
263
+ )
264
+ ) {
265
+ return current
266
+ }
267
+
268
+ if (
269
+ current ===
270
+ filesystemRoot
271
+ ) {
272
+ break
273
+ }
274
+
275
+ const parent =
276
+ path.dirname(
277
+ current
278
+ )
279
+
280
+ if (
281
+ parent === current
282
+ ) {
283
+ break
284
+ }
285
+
286
+ current = parent
287
+ }
288
+
289
+ return null
290
+ }
291
+
292
+ function getSessionId(
293
+ event
294
+ ) {
295
+ const p =
296
+ event?.properties ?? {}
23
297
 
24
298
  const candidates = [
25
299
  p.sessionID,
@@ -28,155 +302,763 @@ function getSessionId(event) {
28
302
  p.session?.id,
29
303
  ]
30
304
 
31
- return candidates.find(
32
- value => typeof value === "string" && value.length > 0
33
- ) ?? null
305
+ return (
306
+ candidates.find(
307
+ value =>
308
+ typeof value ===
309
+ "string" &&
310
+ value.length > 0
311
+ ) ?? null
312
+ )
34
313
  }
35
314
 
36
- export const ToolNetMemoryPlugin = async ({ directory }) => {
37
- const binary = ${JSON.stringify(r)}
38
-
39
- async function refreshContext() {
40
- try {
41
- const child = Bun.spawn(
42
- [
43
- binary,
44
- "context:sync",
45
- "--project",
46
- directory,
47
- ],
48
- {
49
- stdin: "ignore",
50
- stdout: "ignore",
51
- stderr: "ignore",
52
- }
315
+ async function runWithTimeout(
316
+ args,
317
+ {
318
+ stdout = "ignore",
319
+ timeout =
320
+ REMOTE_TIMEOUT_MS,
321
+ } = {}
322
+ ) {
323
+ const child =
324
+ Bun.spawn(
325
+ args,
326
+ {
327
+ stdin:
328
+ "ignore",
329
+ stdout,
330
+ stderr:
331
+ "ignore",
332
+ }
333
+ )
334
+
335
+ let timer
336
+
337
+ const timeoutPromise =
338
+ new Promise(
339
+ (_, reject) => {
340
+ timer =
341
+ setTimeout(
342
+ () => {
343
+ try {
344
+ child.kill()
345
+ } catch {}
346
+
347
+ reject(
348
+ new Error(
349
+ "ToolNet command timeout"
350
+ )
351
+ )
352
+ },
353
+ timeout
354
+ )
355
+ }
356
+ )
357
+
358
+ try {
359
+ const exitCode =
360
+ await Promise.race([
361
+ child.exited,
362
+ timeoutPromise,
363
+ ])
364
+
365
+ if (
366
+ typeof exitCode ===
367
+ "number" &&
368
+ exitCode !== 0
369
+ ) {
370
+ throw new Error(
371
+ \`ToolNet exited with code \${exitCode}\`
53
372
  )
373
+ }
54
374
 
55
- await child.exited
56
- } catch {
57
- // Context sync must never break OpenCode startup.
375
+ return child
376
+ } finally {
377
+ if (timer) {
378
+ clearTimeout(
379
+ timer
380
+ )
58
381
  }
59
382
  }
383
+ }
60
384
 
61
- async function readContext() {
62
- try {
63
- const child = Bun.spawn(
64
- [
65
- binary,
66
- "context:print",
67
- "--project",
68
- directory,
69
- "--tokens",
70
- "900",
71
- ],
72
- {
73
- stdin: "ignore",
74
- stdout: "pipe",
75
- stderr: "ignore",
76
- }
385
+ export const ToolNetMemoryPlugin =
386
+ async ({
387
+ directory,
388
+ }) => {
389
+ /*
390
+ * CRITICAL:
391
+ * Never auto-create a ToolNet
392
+ * project just because OpenCode
393
+ * happens to start in /root,
394
+ * /tmp, $HOME, etc.
395
+ */
396
+ const projectRoot =
397
+ findProjectRoot(
398
+ directory
77
399
  )
78
400
 
79
- const text =
80
- await new Response(
81
- child.stdout
82
- ).text()
401
+ if (!projectRoot) {
402
+ writeStatus({
403
+ active: false,
404
+ reason:
405
+ "no-toolnet-project",
406
+ directory,
407
+ })
408
+
409
+ return {}
410
+ }
411
+
412
+ writeStatus({
413
+ active: true,
414
+ projectRoot,
415
+ state:
416
+ "plugin-loaded",
417
+ })
418
+
419
+ let lastSessionId =
420
+ null
421
+
422
+ /*
423
+ * Two independent lanes:
424
+ *
425
+ * captureChain:
426
+ * OpenCode DB -> local fsync WAL -> current work
427
+ *
428
+ * remoteInFlight:
429
+ * WAL -> Hugging Face / S3
430
+ *
431
+ * A slow remote backend must never block local capture.
432
+ */
433
+ let captureChain =
434
+ Promise.resolve()
83
435
 
84
- await child.exited
436
+ let remoteInFlight =
437
+ null
85
438
 
86
- return text.trim()
87
- } catch {
88
- return ""
439
+ let debounceTimer =
440
+ null
441
+
442
+ let contextCache = {
443
+ value: "",
444
+ expiresAt: 0,
89
445
  }
90
- }
91
446
 
92
- function sync(sessionId, flag) {
93
- if (!sessionId) return
447
+ /*
448
+ * Compact startup context is injected once
449
+ * per native OpenCode session.
450
+ *
451
+ * Deep history is retrieved through
452
+ * memory_agent_ask / MCP on demand.
453
+ */
454
+ const injectedSessions =
455
+ new Set()
456
+
457
+ function contextSessionKey(
458
+ input
459
+ ) {
460
+ const candidates = [
461
+ input?.sessionID,
462
+ input?.sessionId,
463
+ input?.session?.id,
464
+ input?.info?.id,
465
+ lastSessionId,
466
+ ]
467
+
468
+ const sessionId =
469
+ candidates.find(
470
+ value =>
471
+ typeof value ===
472
+ "string" &&
473
+ value.length > 0
474
+ )
475
+
476
+ return sessionId
477
+ ? "session:" +
478
+ sessionId
479
+ : "project:" +
480
+ projectRoot
481
+ }
482
+
483
+ async function readContext() {
484
+ const now =
485
+ Date.now()
486
+
487
+ if (
488
+ contextCache.value &&
489
+ now <
490
+ contextCache.expiresAt
491
+ ) {
492
+ return (
493
+ contextCache.value
494
+ )
495
+ }
496
+
497
+ try {
498
+ const child =
499
+ Bun.spawn(
500
+ [
501
+ TOOLNET_BINARY,
502
+ "context:print",
503
+ "--project",
504
+ projectRoot,
505
+ "--tokens",
506
+ String(
507
+ CONTEXT_MAX_TOKENS
508
+ ),
509
+ ],
510
+ {
511
+ stdin:
512
+ "ignore",
513
+ stdout:
514
+ "pipe",
515
+ stderr:
516
+ "ignore",
517
+ }
518
+ )
519
+
520
+ const textPromise =
521
+ new Response(
522
+ child.stdout
523
+ ).text()
524
+
525
+ const exitCode =
526
+ await child.exited
527
+
528
+ if (
529
+ exitCode !== 0
530
+ ) {
531
+ return ""
532
+ }
533
+
534
+ const text =
535
+ (
536
+ await textPromise
537
+ ).trim()
538
+
539
+ contextCache = {
540
+ value: text,
541
+ expiresAt:
542
+ now +
543
+ CONTEXT_CACHE_MS,
544
+ }
545
+
546
+ return text
547
+ } catch {
548
+ return ""
549
+ }
550
+ }
551
+
552
+ async function syncNow(
553
+ sessionId,
554
+ {
555
+ flag = null,
556
+ reason = "unknown",
557
+ localOnly = false,
558
+ timeout =
559
+ REMOTE_TIMEOUT_MS,
560
+ } = {}
561
+ ) {
562
+ if (!sessionId) {
563
+ return
564
+ }
94
565
 
95
- try {
96
566
  const args = [
97
- binary,
567
+ TOOLNET_BINARY,
98
568
  "session:opencode-sync",
99
569
  sessionId,
100
570
  "--project",
101
- directory,
571
+ projectRoot,
102
572
  ]
103
573
 
104
574
  if (flag) {
105
- args.push(flag)
575
+ args.push(
576
+ flag
577
+ )
106
578
  }
107
579
 
108
- const child = Bun.spawn(args, {
109
- stdin: "ignore",
110
- stdout: "ignore",
111
- stderr: "ignore",
112
- })
580
+ if (localOnly) {
581
+ args.push(
582
+ "--local-only"
583
+ )
584
+ }
113
585
 
114
- if (
115
- typeof child.unref === "function"
116
- ) {
117
- child.unref()
586
+ try {
587
+ await runWithTimeout(
588
+ args,
589
+ {
590
+ timeout,
591
+ }
592
+ )
593
+
594
+ writeStatus({
595
+ active: true,
596
+ projectRoot,
597
+ sessionId,
598
+ reason,
599
+ mode:
600
+ localOnly
601
+ ? "local"
602
+ : "remote",
603
+ state:
604
+ localOnly
605
+ ? "capture-success"
606
+ : "sync-success",
607
+ })
608
+ } catch (error) {
609
+ writeStatus({
610
+ active: true,
611
+ projectRoot,
612
+ sessionId,
613
+ reason,
614
+ mode:
615
+ localOnly
616
+ ? "local"
617
+ : "remote",
618
+ state:
619
+ localOnly
620
+ ? "capture-failed"
621
+ : "sync-failed",
622
+ error:
623
+ error instanceof
624
+ Error
625
+ ? error.message
626
+ : String(
627
+ error
628
+ ),
629
+ })
630
+
631
+ throw error
118
632
  }
119
- } catch {
120
- // Memory capture must never break OpenCode.
121
633
  }
122
- }
123
634
 
124
- /*
125
- * Pull the latest project brief from ToolNet remote storage
126
- * before the new OpenCode session starts doing work.
127
- */
128
- await refreshContext()
635
+ function queueCapture(
636
+ sessionId,
637
+ flag,
638
+ reason
639
+ ) {
640
+ if (!sessionId) {
641
+ return captureChain
642
+ }
129
643
 
130
- return {
131
- event: async ({ event }) => {
132
- const sessionId =
133
- getSessionId(event)
644
+ lastSessionId =
645
+ sessionId
646
+
647
+ /*
648
+ * Local captures are serialized with each other,
649
+ * but completely independent from remote sync.
650
+ */
651
+ captureChain =
652
+ captureChain
653
+ .catch(
654
+ () =>
655
+ undefined
656
+ )
657
+ .then(
658
+ () =>
659
+ syncNow(
660
+ sessionId,
661
+ {
662
+ flag,
663
+ reason,
664
+ localOnly: true,
665
+ timeout:
666
+ CAPTURE_TIMEOUT_MS,
667
+ }
668
+ )
669
+ )
670
+ .catch(
671
+ () =>
672
+ undefined
673
+ )
674
+
675
+ return captureChain
676
+ }
134
677
 
678
+ function queueRemote(
679
+ sessionId,
680
+ flag,
681
+ reason
682
+ ) {
135
683
  if (!sessionId) {
136
- return
684
+ return Promise.resolve()
137
685
  }
138
686
 
139
- if (event.type === "session.idle") {
140
- sync(sessionId, "--idle")
141
- return
687
+ lastSessionId =
688
+ sessionId
689
+
690
+ /*
691
+ * Never build an endless remote backlog.
692
+ * One remote flush is enough because WAL keeps
693
+ * every pending local event until acknowledged.
694
+ */
695
+ if (remoteInFlight) {
696
+ return remoteInFlight
142
697
  }
143
698
 
144
- if (event.type === "session.compacted") {
145
- sync(sessionId, "--compacted")
699
+ remoteInFlight =
700
+ syncNow(
701
+ sessionId,
702
+ {
703
+ flag,
704
+ reason,
705
+ localOnly: false,
706
+ timeout:
707
+ REMOTE_TIMEOUT_MS,
708
+ }
709
+ )
710
+ .catch(
711
+ () =>
712
+ undefined
713
+ )
714
+ .finally(
715
+ () => {
716
+ remoteInFlight =
717
+ null
718
+ }
719
+ )
720
+
721
+ return remoteInFlight
722
+ }
723
+
724
+ function scheduleCapture(
725
+ sessionId,
726
+ reason
727
+ ) {
728
+ if (!sessionId) {
146
729
  return
147
730
  }
148
731
 
149
- if (event.type === "session.error") {
150
- sync(sessionId, "--error")
732
+ if (debounceTimer) {
733
+ clearTimeout(
734
+ debounceTimer
735
+ )
736
+ }
737
+
738
+ debounceTimer =
739
+ setTimeout(
740
+ () => {
741
+ debounceTimer =
742
+ null
743
+
744
+ void queueCapture(
745
+ sessionId,
746
+ null,
747
+ reason
748
+ )
749
+ },
750
+ EVENT_CAPTURE_DEBOUNCE_MS
751
+ )
752
+
753
+ if (
754
+ typeof debounceTimer.unref ===
755
+ "function"
756
+ ) {
757
+ debounceTimer.unref()
151
758
  }
152
- },
759
+ }
153
760
 
154
761
  /*
155
- * Official OpenCode compaction hook.
156
- * Preserve ToolNet's semantic state after context compaction.
762
+ * Crash-safe LOCAL checkpoint.
763
+ *
764
+ * Fast lane:
765
+ * OpenCode DB -> fsync WAL -> current work.
766
+ *
767
+ * No network dependency.
157
768
  */
158
- "experimental.session.compacting": async (_input, output) => {
159
- const context =
160
- await readContext()
769
+ const localPeriodic =
770
+ setInterval(
771
+ () => {
772
+ if (
773
+ lastSessionId
774
+ ) {
775
+ void queueCapture(
776
+ lastSessionId,
777
+ null,
778
+ "periodic-local"
779
+ )
780
+ }
781
+ },
782
+ LOCAL_CAPTURE_MS
783
+ )
784
+
785
+ /*
786
+ * Remote durability is deliberately slower
787
+ * and independent from the local lane.
788
+ */
789
+ const remotePeriodic =
790
+ setInterval(
791
+ () => {
792
+ if (
793
+ lastSessionId
794
+ ) {
795
+ void queueRemote(
796
+ lastSessionId,
797
+ null,
798
+ "periodic-remote"
799
+ )
800
+ }
801
+ },
802
+ REMOTE_SYNC_MS
803
+ )
161
804
 
805
+ for (
806
+ const timer of [
807
+ localPeriodic,
808
+ remotePeriodic,
809
+ ]
810
+ ) {
162
811
  if (
163
- context &&
164
- Array.isArray(
165
- output.context
166
- )
812
+ typeof timer.unref ===
813
+ "function"
167
814
  ) {
168
- output.context.push(
169
- context
170
- )
815
+ timer.unref()
171
816
  }
172
- },
817
+ }
818
+
819
+ return {
820
+ event: async ({
821
+ event,
822
+ }) => {
823
+ const sessionId =
824
+ getSessionId(
825
+ event
826
+ )
827
+
828
+ if (sessionId) {
829
+ lastSessionId =
830
+ sessionId
831
+ }
832
+
833
+ const terminalEvent =
834
+ event.type ===
835
+ "session.idle" ||
836
+ event.type ===
837
+ "session.compacted" ||
838
+ event.type ===
839
+ "session.error"
840
+
841
+ if (
842
+ sessionId &&
843
+ !terminalEvent
844
+ ) {
845
+ scheduleCapture(
846
+ sessionId,
847
+ "event:" +
848
+ event.type
849
+ )
850
+ }
851
+
852
+ if (
853
+ event.type ===
854
+ "session.idle"
855
+ ) {
856
+ await queueCapture(
857
+ sessionId,
858
+ "--idle",
859
+ "session.idle:capture"
860
+ )
861
+
862
+ void queueRemote(
863
+ sessionId,
864
+ null,
865
+ "session.idle:remote"
866
+ )
867
+
868
+ return
869
+ }
870
+
871
+ if (
872
+ event.type ===
873
+ "session.compacted"
874
+ ) {
875
+ await queueCapture(
876
+ sessionId,
877
+ "--compacted",
878
+ "session.compacted:capture"
879
+ )
880
+
881
+ void queueRemote(
882
+ sessionId,
883
+ null,
884
+ "session.compacted:remote"
885
+ )
886
+
887
+ contextCache = {
888
+ value: "",
889
+ expiresAt: 0,
890
+ }
891
+
892
+ return
893
+ }
894
+
895
+ if (
896
+ event.type ===
897
+ "session.error"
898
+ ) {
899
+ await queueCapture(
900
+ sessionId,
901
+ "--error",
902
+ "session.error:capture"
903
+ )
904
+
905
+ void queueRemote(
906
+ sessionId,
907
+ null,
908
+ "session.error:remote"
909
+ )
910
+
911
+ return
912
+ }
913
+ },
914
+
915
+ /*
916
+ * VERIFIED on OpenCode 1.18.14:
917
+ * this hook fires on real model turns.
918
+ *
919
+ * It remains experimental, therefore
920
+ * AGENTS.md + MCP stay as fallbacks.
921
+ */
922
+ "experimental.chat.system.transform":
923
+ async (
924
+ input,
925
+ output
926
+ ) => {
927
+ const injectionKey =
928
+ contextSessionKey(
929
+ input
930
+ )
931
+
932
+ /*
933
+ * Never inject ToolNet context on
934
+ * every model turn.
935
+ */
936
+ if (
937
+ injectedSessions.has(
938
+ injectionKey
939
+ )
940
+ ) {
941
+ return
942
+ }
943
+
944
+ const context =
945
+ await readContext()
946
+
947
+ if (!context) {
948
+ return
949
+ }
950
+
951
+ if (
952
+ Array.isArray(
953
+ output?.system
954
+ )
955
+ ) {
956
+ output.system.push(
957
+ context
958
+ )
959
+
960
+ injectedSessions.add(
961
+ injectionKey
962
+ )
963
+
964
+ return
965
+ }
966
+
967
+ if (
968
+ typeof output?.system ===
969
+ "string"
970
+ ) {
971
+ output.system =
972
+ output.system
973
+ ? output.system +
974
+ "\\n\\n" +
975
+ context
976
+ : context
977
+
978
+ injectedSessions.add(
979
+ injectionKey
980
+ )
981
+ }
982
+ },
983
+
984
+ /*
985
+ * Optional compaction survival hook.
986
+ * We keep it as a supplementary path,
987
+ * never as the only continuity path.
988
+ */
989
+ "experimental.session.compacting":
990
+ async (
991
+ _input,
992
+ output
993
+ ) => {
994
+ const context =
995
+ await readContext()
996
+
997
+ if (
998
+ context &&
999
+ Array.isArray(
1000
+ output?.context
1001
+ )
1002
+ ) {
1003
+ output.context.push(
1004
+ context
1005
+ )
1006
+ }
1007
+ },
1008
+
1009
+ /*
1010
+ * VERIFIED for clean OpenCode exits.
1011
+ * Best effort only: kill -9 / crash /
1012
+ * machine loss cannot guarantee this.
1013
+ */
1014
+ dispose: async () => {
1015
+ clearInterval(
1016
+ localPeriodic
1017
+ )
1018
+
1019
+ clearInterval(
1020
+ remotePeriodic
1021
+ )
1022
+
1023
+ if (debounceTimer) {
1024
+ clearTimeout(
1025
+ debounceTimer
1026
+ )
1027
+ }
1028
+
1029
+ /*
1030
+ * On a clean exit we only REQUIRE the
1031
+ * fast local fsync checkpoint.
1032
+ *
1033
+ * Never hold OpenCode shutdown hostage
1034
+ * to a slow remote backend.
1035
+ */
1036
+ if (
1037
+ lastSessionId
1038
+ ) {
1039
+ await queueCapture(
1040
+ lastSessionId,
1041
+ "--idle",
1042
+ "dispose:capture"
1043
+ )
1044
+ }
1045
+
1046
+ try {
1047
+ await captureChain
1048
+ } catch {
1049
+ // Fail open.
1050
+ }
1051
+ },
1052
+ }
173
1053
  }
174
- }
175
- `;return C(e,n,{encoding:"utf8",mode:384}),e}import{existsSync as Y,mkdirSync as N,readFileSync as X,writeFileSync as T}from"node:fs";import{homedir as E}from"node:os";import{dirname as w,join as m}from"node:path";function q(o){let t=[],e=/"((?:\\.|[^"\\])*)"|'([^']*)'/g,r;for(;r=e.exec(o);){let n=r[1]??r[2]??"";try{t.push(r[1]!==void 0?JSON.parse(`"${n}"`):n)}catch{t.push(n)}}return t}function j(o={}){let t=o.configFile??m(process.env.CODEX_HOME??m(E(),".codex"),"config.toml"),e=o.previousFile??m(E(),".config","toolnet-memory","codex-notify-previous.json");N(w(t),{recursive:!0}),N(w(e),{recursive:!0});let r=Y(t)?X(t,"utf8"):"",n=o.binary??"toolnet-memory",a=`notify = [${JSON.stringify(n)}, "session:codex-notify"]`,s=r.split(`
176
- `),i=s.findIndex(c=>/^\s*\[/.test(c));i<0&&(i=s.length);let u=-1,d=-1;for(let c=0;c<i;c+=1)if(/^\s*notify\s*=/.test(s[c])){if(u=c,d=c,s[c].includes("[")&&!s[c].includes("]"))for(;d+1<i&&(d+=1,!s[d].includes("]")););break}let l=[];if(u>=0){let c=s.slice(u,d+1).join(`
177
- `);l=q(c),s.splice(u,d-u+1,a)}else i=s.findIndex(c=>/^\s*\[/.test(c)),i<0&&(i=s.length),s.splice(i,0,a);let O=l.length>=2&&l[l.length-1]==="session:codex-notify";return l.length>0&&!O&&T(e,JSON.stringify(l,null,2)+`
178
- `,{encoding:"utf8",mode:384}),r=s.join(`
179
- `),r.endsWith(`
180
- `)||(r+=`
181
- `),T(t,r,{encoding:"utf8",mode:384}),{configFile:t,previousFile:e,preservedPrevious:l.length>0&&!O}}import{existsSync as G,mkdirSync as U,readFileSync as V,writeFileSync as z}from"node:fs";import{homedir as K}from"node:os";import{dirname as Q,join as A}from"node:path";function Z(o){return`'${o.replace(/'/g,"'\\''")}'`}function F(o={}){let t=o.hooksFile??A(process.env.CODEX_HOME??A(K(),".codex"),"hooks.json");U(Q(t),{recursive:!0});let e={};if(G(t))try{e=JSON.parse(V(t,"utf8"))}catch(i){throw new Error(`Invalid existing Codex hooks.json: ${i instanceof Error?i.message:String(i)}`)}let r=e.hooks&&typeof e.hooks=="object"&&!Array.isArray(e.hooks)?e.hooks:{};e.hooks=r;let a=(Array.isArray(r.SessionStart)?r.SessionStart:[]).filter(i=>{try{return!JSON.stringify(i).includes("session:codex-context")}catch{return!0}}),s=o.binary??"toolnet-memory";return a.push({matcher:"startup|resume|clear|compact",hooks:[{type:"command",command:`${Z(s)} session:codex-context`,timeout:15,additionalContextLimit:1e3,statusMessage:"Loading ToolNet project continuity"}]}),r.SessionStart=a,z(t,JSON.stringify(e,null,2)+`
182
- `,{encoding:"utf8",mode:384}),t}function v(o){return ee("sh",["-lc",`command -v ${JSON.stringify(o)} >/dev/null 2>&1`],{stdio:"ignore"}).status===0}function te(){return v("agy")||y(x(h(),".gemini"))}function oe(){return v("opencode")||y(x(h(),".config","opencode"))}function ne(){return v("codex")||y(process.env.CODEX_HOME??x(h(),".codex"))}function re(o={}){let t=o.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",e=[];if(!(o.force===!0||te()))e.push({agent:"agy",detected:!1,installed:!1,targets:[]});else try{let n=S({binary:t});e.push({agent:"agy",detected:!0,installed:!0,targets:[n]})}catch(n){e.push({agent:"agy",detected:!0,installed:!1,targets:[],error:n instanceof Error?n.message:String(n)})}if(!(o.force===!0||oe()))e.push({agent:"opencode",detected:!1,installed:!1,targets:[]});else try{let n=I({binary:t});e.push({agent:"opencode",detected:!0,installed:!0,targets:[n]})}catch(n){e.push({agent:"opencode",detected:!0,installed:!1,targets:[],error:n instanceof Error?n.message:String(n)})}if(!(o.force===!0||ne()))e.push({agent:"codex",detected:!1,installed:!1,targets:[]});else try{let n=j({binary:t}),a=F({binary:t}),s=[n.configFile,a];n.preservedPrevious&&s.push(n.previousFile),e.push({agent:"codex",detected:!0,installed:!0,targets:s})}catch(n){e.push({agent:"codex",detected:!0,installed:!1,targets:[],error:n instanceof Error?n.message:String(n)})}return e}function se(o){console.log(""),console.log("ToolNet Memory AI Integrations"),console.log("=============================="),console.log("");for(let t of o){let e=t.agent==="agy"?"Agy / Antigravity":t.agent==="opencode"?"OpenCode":"Codex";if(!t.detected){console.log(`- ${e}: not detected`);continue}if(t.installed){console.log(`\u2713 ${e}: automatic memory enabled`);continue}console.log(`\u2717 ${e}: integration failed`),t.error&&console.log(` ${t.error}`)}console.log("")}async function ie(){let o=process.argv.slice(2),t=o.includes("--all"),e=o.includes("--json"),r=re({force:t});if(e){console.log(JSON.stringify(r,null,2));return}se(r)}var ce=process.argv[1]&&(process.argv[1].endsWith("auto-integrate.js")||process.argv[1].endsWith("auto-integrate.ts"));ce&&ie().catch(o=>{console.error(o instanceof Error?o.message:String(o)),process.exitCode=1});export{re as installAutoIntegrations};
1054
+ `;return se(t,i.trimStart(),{encoding:"utf8",mode:384}),t}import{existsSync as le,mkdirSync as ct,readFileSync as at,renameSync as lt,writeFileSync as ut}from"node:fs";import{dirname as ue,join as dt}from"node:path";function f(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function mt(e,n){ct(ue(e),{recursive:!0});let t=`${e}.tmp-${process.pid}-${Date.now()}`;ut(t,`${JSON.stringify(n,null,2)}
1055
+ `,{encoding:"utf8",mode:384}),lt(t,e)}function ce(e){if(!le(e))return{};let n=at(e,"utf8").trim();if(!n)return{};let t;try{t=JSON.parse(n)}catch(o){throw new Error(`Invalid existing OpenCode opencode.json: ${o instanceof Error?o.message:String(o)}`)}if(!f(t))throw new Error("Invalid existing OpenCode opencode.json: root must be a JSON object.");return t}function ae(e,n){if(!f(e))return!1;let t=e.command;return e.type==="local"&&e.enabled!==!1&&Array.isArray(t)&&t.length===2&&t[0]===n&&t[1]==="mcp"}function gt(e,n){let t=e.mcpServers;if(!f(t)||!Object.prototype.hasOwnProperty.call(t,n))return{root:e,changed:!1};let o={...t};return delete o[n],{root:{...e,mcpServers:o},changed:!0}}function de(e={}){let n=e.configFile??$(),t=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",o=e.serverName??"toolnet-memory",i=dt(ue(n),"opencode.jsonc"),s=le(i)?i:void 0,r=ce(n),c=gt(r,o),l=c.root,u=l.mcp;if(u!==void 0&&!f(u))throw new Error("Invalid existing OpenCode config: mcp must be an object.");let a=f(u)?{...u}:{},g=a[o];if(ae(g,t)&&!c.changed)return{installed:!0,changed:!1,configFile:n,serverName:o,command:[t,"mcp"],preservedJsonc:s};a[o]={type:"local",command:[t,"mcp"],enabled:!0};let d={...l,mcp:a};mt(n,d);let R=ce(n);if(!f(R.mcp)||!ae(R.mcp[o],t))throw new Error("OpenCode MCP configuration was written but verification failed.");return{installed:!0,changed:!0,configFile:n,serverName:o,command:[t,"mcp"],preservedJsonc:s}}import{existsSync as pt,mkdirSync as me,readFileSync as ft,writeFileSync as ge}from"node:fs";import{homedir as pe}from"node:os";import{dirname as fe,join as j}from"node:path";function yt(e){let n=[],t=/"((?:\\.|[^"\\])*)"|'([^']*)'/g,o;for(;o=t.exec(e);){let i=o[1]??o[2]??"";try{n.push(o[1]!==void 0?JSON.parse(`"${i}"`):i)}catch{n.push(i)}}return n}function ye(e={}){let n=e.configFile??j(process.env.CODEX_HOME??j(pe(),".codex"),"config.toml"),t=e.previousFile??j(pe(),".config","toolnet-memory","codex-notify-previous.json");me(fe(n),{recursive:!0}),me(fe(t),{recursive:!0});let o=pt(n)?ft(n,"utf8"):"",i=e.binary??"toolnet-memory",s=`notify = [${JSON.stringify(i)}, "session:codex-notify"]`,r=o.split(`
1056
+ `),c=r.findIndex(d=>/^\s*\[/.test(d));c<0&&(c=r.length);let l=-1,u=-1;for(let d=0;d<c;d+=1)if(/^\s*notify\s*=/.test(r[d])){if(l=d,u=d,r[d].includes("[")&&!r[d].includes("]"))for(;u+1<c&&(u+=1,!r[u].includes("]")););break}let a=[];if(l>=0){let d=r.slice(l,u+1).join(`
1057
+ `);a=yt(d),r.splice(l,u-l+1,s)}else c=r.findIndex(d=>/^\s*\[/.test(d)),c<0&&(c=r.length),r.splice(c,0,s);let g=a.length>=2&&a[a.length-1]==="session:codex-notify";return a.length>0&&!g&&ge(t,JSON.stringify(a,null,2)+`
1058
+ `,{encoding:"utf8",mode:384}),o=r.join(`
1059
+ `),o.endsWith(`
1060
+ `)||(o+=`
1061
+ `),ge(n,o,{encoding:"utf8",mode:384}),{configFile:n,previousFile:t,preservedPrevious:a.length>0&&!g}}import{existsSync as ht,mkdirSync as Ot,readFileSync as vt,writeFileSync as xt}from"node:fs";import{homedir as Ct}from"node:os";import{dirname as St,join as he}from"node:path";function bt(e){return`'${e.replace(/'/g,"'\\''")}'`}function Oe(e={}){let n=e.hooksFile??he(process.env.CODEX_HOME??he(Ct(),".codex"),"hooks.json");Ot(St(n),{recursive:!0});let t={};if(ht(n))try{t=JSON.parse(vt(n,"utf8"))}catch(c){throw new Error(`Invalid existing Codex hooks.json: ${c instanceof Error?c.message:String(c)}`)}let o=t.hooks&&typeof t.hooks=="object"&&!Array.isArray(t.hooks)?t.hooks:{};t.hooks=o;let s=(Array.isArray(o.SessionStart)?o.SessionStart:[]).filter(c=>{try{return!JSON.stringify(c).includes("session:codex-context")}catch{return!0}}),r=e.binary??"toolnet-memory";return s.push({matcher:"startup|resume|clear|compact",hooks:[{type:"command",command:`${bt(r)} session:codex-context`,timeout:15,additionalContextLimit:1e3,statusMessage:"Loading ToolNet project continuity"}]}),o.SessionStart=s,xt(n,JSON.stringify(t,null,2)+`
1062
+ `,{encoding:"utf8",mode:384}),n}import{spawnSync as wt}from"node:child_process";function A(e,n){return wt(e,n,{encoding:"utf8",stdio:["ignore","pipe","pipe"]})}function ve(e,n){let t=A(e,["mcp","get",n,"--json"]);if(t.status!==0||!t.stdout)return null;try{return JSON.parse(t.stdout)}catch{return null}}function xe(e,n){return e.enabled!==!1&&e.transport?.type==="stdio"&&e.transport?.command===n&&Array.isArray(e.transport?.args)&&e.transport?.args.length===1&&e.transport.args[0]==="mcp"}function Ce(e={}){let n=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",t=e.codexBinary??"codex",o=e.serverName??"toolnet-memory",i=ve(t,o);if(i&&xe(i,n))return{installed:!0,changed:!1,serverName:o,command:n,args:["mcp"]};if(i){let c=A(t,["mcp","remove",o]);if(c.status!==0)return{installed:!1,changed:!1,serverName:o,command:n,args:["mcp"],error:(c.stderr||c.stdout||"Unable to remove old ToolNet MCP configuration.").trim()}}let s=A(t,["mcp","add",o,"--",n,"mcp"]);if(s.status!==0)return{installed:!1,changed:!1,serverName:o,command:n,args:["mcp"],error:(s.stderr||s.stdout||"Unable to register ToolNet MCP.").trim()};let r=ve(t,o);return!r||!xe(r,n)?{installed:!1,changed:!0,serverName:o,command:n,args:["mcp"],error:"Codex accepted MCP registration but verification did not match expected ToolNet command."}:{installed:!0,changed:!0,serverName:o,command:n,args:["mcp"]}}import{existsSync as It,mkdirSync as kt,readFileSync as Nt,renameSync as Tt,rmSync as Et,writeFileSync as jt}from"node:fs";import{dirname as At}from"node:path";function O(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Mt(e){return/^[A-Za-z0-9_./:-]+$/u.test(e)?e:`'${e.replace(/'/gu,"'\\''")}'`}function Ft(e){if(!It(e))return{};let n;try{n=JSON.parse(Nt(e,"utf8"))}catch(t){throw new Error(`Invalid existing Claude settings.json: ${t instanceof Error?t.message:String(t)}`)}if(!O(n))throw new Error("Invalid existing Claude settings.json: root must be a JSON object.");return n}function M(e){if(e===void 0)return[];if(!Array.isArray(e))throw new Error("Invalid existing Claude settings.json: hook event must be an array.");let n=[];for(let t of e){if(!O(t)){n.push(t);continue}let o=t.hooks;if(!Array.isArray(o)){n.push(t);continue}let i=o.filter(s=>{if(!O(s))return!0;let r=s.command;return!(typeof r=="string"&&r.includes("session:claude-hook"))});i.length!==0&&n.push({...t,hooks:i})}return n}function F(e){return{type:"command",command:e,timeout:10}}function Rt(e,n){kt(At(e),{recursive:!0,mode:448});let t=`${e}.toolnet-${process.pid}-${Date.now()}.tmp`;try{jt(t,JSON.stringify(n,null,2)+`
1063
+ `,{encoding:"utf8",mode:384}),Tt(t,e)}finally{Et(t,{force:!0})}}function Se(e={}){let n=e.settingsFile??U(),t=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",o=Ft(n),i=o.hooks;if(i!==void 0&&!O(i))throw new Error("Invalid existing Claude settings.json: hooks must be an object.");let s=O(i)?{...i}:{},r=`${Mt(t)} session:claude-hook`,c=M(s.SessionStart);c.push({matcher:"startup|resume|clear|compact",hooks:[F(r)]}),s.SessionStart=c;let l=M(s.PostToolUse);l.push({matcher:"Edit|Write",hooks:[F(r)]}),s.PostToolUse=l;let u=M(s.Stop);u.push({hooks:[F(r)]}),s.Stop=u;let a={...o,hooks:s},g=JSON.stringify(o),d=JSON.stringify(a);return g===d?{settingsFile:n,changed:!1}:(Rt(n,a),{settingsFile:n,changed:!0})}import{existsSync as _t,mkdirSync as Pt,readFileSync as Jt,renameSync as Dt,rmSync as $t,writeFileSync as Lt}from"node:fs";import{dirname as Ht}from"node:path";function v(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function be(e){if(!_t(e))return{};let n;try{n=JSON.parse(Jt(e,"utf8"))}catch(t){throw new Error(`Invalid existing Claude Code config: ${t instanceof Error?t.message:String(t)}`)}if(!v(n))throw new Error("Invalid existing Claude Code config: root must be a JSON object.");return n}function we(e,n){if(!v(e))return!1;let t=e.args;return e.type==="stdio"&&e.command===n&&Array.isArray(t)&&t.length===1&&t[0]==="mcp"}function Bt(e,n){Pt(Ht(e),{recursive:!0});let t=`${e}.toolnet-${process.pid}-${Date.now()}.tmp`;try{Lt(t,JSON.stringify(n,null,2)+`
1064
+ `,{encoding:"utf8",mode:384}),Dt(t,e)}finally{$t(t,{force:!0})}}function Ie(e={}){let n=e.stateFile??Y(),t=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",o=e.serverName??"toolnet-memory",i=be(n),s=i.mcpServers;if(s!==void 0&&!v(s))throw new Error("Invalid existing Claude Code config: mcpServers must be an object.");let r=v(s)?{...s}:{},c=r[o];if(we(c,t))return{installed:!0,changed:!1,configFile:n,serverName:o,command:[t,"mcp"],repaired:!1};let l=c!==void 0;r[o]={type:"stdio",command:t,args:["mcp"]},Bt(n,{...i,mcpServers:r});let a=be(n).mcpServers;if(!v(a)||!we(a[o],t))throw new Error("Claude Code MCP configuration was written but verification failed.");return{installed:!0,changed:!0,configFile:n,serverName:o,command:[t,"mcp"],repaired:l}}function ke(e={}){let n=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",t=Se({binary:n,settingsFile:e.settingsFile}),o=Ie({binary:n,stateFile:e.stateFile});return{hooks:t,mcp:o,files:[t.settingsFile,o.configFile]}}function Ne(){return q()}function Ut(e={}){let n=e.binary??process.env.TOOLNET_MEMORY_BIN??"toolnet-memory",t=[],o=Ne(),i=new Map(o.map(s=>[s.agent,s.detected]));if(!(e.force===!0||i.get("agy")===!0))t.push({agent:"agy",detected:!1,installed:!1,targets:[]});else try{let r=te({binary:n});t.push({agent:"agy",detected:!0,installed:!0,targets:r.files})}catch(r){t.push({agent:"agy",detected:!0,installed:!1,targets:[],error:r instanceof Error?r.message:String(r)})}if(!(e.force===!0||i.get("opencode")===!0))t.push({agent:"opencode",detected:!1,installed:!1,targets:[]});else try{let r=ie({binary:n}),c=de({binary:n});t.push({agent:"opencode",detected:!0,installed:!0,targets:[r,c.configFile,`mcp:${c.serverName}`]})}catch(r){t.push({agent:"opencode",detected:!0,installed:!1,targets:[],error:r instanceof Error?r.message:String(r)})}if(!(e.force===!0||i.get("claude")===!0))t.push({agent:"claude",detected:!1,installed:!1,targets:[]});else try{let r=ke({binary:n});t.push({agent:"claude",detected:!0,installed:!0,targets:[r.hooks.settingsFile,r.mcp.configFile,`mcp:${r.mcp.serverName}`]})}catch(r){t.push({agent:"claude",detected:!0,installed:!1,targets:[],error:r instanceof Error?r.message:String(r)})}if(!(e.force===!0||i.get("codex")===!0))t.push({agent:"codex",detected:!1,installed:!1,targets:[]});else try{let r=ye({binary:n}),c=Oe({binary:n}),l=Ce({binary:n});if(!l.installed)throw new Error(l.error??"Codex MCP registration failed");let u=[r.configFile,c,`mcp:${l.serverName}`];r.preservedPrevious&&u.push(r.previousFile),t.push({agent:"codex",detected:!0,installed:!0,targets:u})}catch(r){t.push({agent:"codex",detected:!0,installed:!1,targets:[],error:r instanceof Error?r.message:String(r)})}return t}function Yt(e){console.log(""),console.log("ToolNet Memory Integration Detection"),console.log("===================================="),console.log("");for(let n of e){let t=n.agent==="agy"?"Agy / Antigravity":n.agent==="opencode"?"OpenCode":n.agent==="claude"?"Claude Code":"Codex";if(!n.detected){console.log(`\u25CB ${t}: not detected`);continue}console.log(`\u2713 ${t}: detected`);for(let o of n.evidence)console.log(` ${o}`)}console.log("")}function qt(e){console.log(""),console.log("ToolNet Memory AI Integrations"),console.log("=============================="),console.log("");for(let n of e){let t=n.agent==="agy"?"Agy / Antigravity":n.agent==="opencode"?"OpenCode":"Codex";if(!n.detected){console.log(`- ${t}: not detected`);continue}if(n.installed){console.log(`\u2713 ${t}: automatic memory enabled`);continue}console.log(`\u2717 ${t}: integration failed`),n.error&&console.log(` ${n.error}`)}console.log("")}async function Wt(){let e=process.argv.slice(2),n=e.includes("--all"),t=e.includes("--json");if(e.includes("--detect-only")){let s=Ne();if(t){console.log(JSON.stringify(s,null,2));return}Yt(s);return}let i=Ut({force:n});if(t){console.log(JSON.stringify(i,null,2));return}qt(i)}var Gt=process.argv[1]&&(process.argv[1].endsWith("auto-integrate.js")||process.argv[1].endsWith("auto-integrate.ts"));Gt&&Wt().catch(e=>{console.error(e instanceof Error?e.message:String(e)),process.exitCode=1});export{Ne as detectAutoIntegrations,Ut as installAutoIntegrations};