pi-odd-worktree 0.1.0 → 1.1.0

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.
@@ -7528,6 +7528,65 @@ function registerIssuesCommand(pi) {
7528
7528
  });
7529
7529
  }
7530
7530
 
7531
+ // ---------------------------------------------------------------------------
7532
+ // `/codegraph-hook`, `/codegraph-init`, `/codegraph-sync` (T5 S9): the sync
7533
+ // hook's switch and the two on-demand CLI runs. The bodies live in
7534
+ // lib/codegraph-commands.js so they are testable without a host; this file only
7535
+ // registers them and hands their one-line result to the session UI.
7536
+ //
7537
+ // GATED REGISTRATION: the three commands do not exist at all in a session whose
7538
+ // gate says the `codegraph` CLI is absent, so they never appear in that
7539
+ // session's command list. The gate caches its answer per process (S3), so
7540
+ // asking at registration time costs one PATH probe for the whole session.
7541
+ function codegraphCommandsAvailable() {
7542
+ try {
7543
+ return libRequire("codegraph-session.js").hasCodegraph(process.env) === true;
7544
+ } catch {
7545
+ return false;
7546
+ }
7547
+ }
7548
+
7549
+ function registerCodegraphCommands(pi) {
7550
+ if (typeof pi?.registerCommand !== "function") return;
7551
+ if (!codegraphCommandsAvailable()) return;
7552
+ const render = (ctx, result) => {
7553
+ const notify = ctx && ctx.ui ? ctx.ui.notify : undefined;
7554
+ if (typeof notify === "function") notify(result.text, result.level);
7555
+ return result;
7556
+ };
7557
+ // A command never throws into the host: the lib returns a failure line
7558
+ // instead of rejecting, and anything unforeseen is swallowed here.
7559
+ const run = async (ctx, work) => {
7560
+ try {
7561
+ render(ctx, await work());
7562
+ } catch {
7563
+ // Nothing to report that the lib did not already describe.
7564
+ }
7565
+ };
7566
+ pi.registerCommand("codegraph-hook", {
7567
+ description: "Turn the automatic edit/write CodeGraph sync on or off: on, off, status (no argument toggles)",
7568
+ handler: async (args, ctx) => {
7569
+ await run(ctx, () => libRequire("codegraph-commands.js").codegraphHook(args));
7570
+ },
7571
+ });
7572
+ pi.registerCommand("codegraph-init", {
7573
+ description: "Initialize a CodeGraph index in this session's checkout: codegraph init .",
7574
+ handler: async (args, ctx) => {
7575
+ await run(ctx, () => libRequire("codegraph-commands.js").codegraphInit(args, ctx, {
7576
+ run: libRunner(libRequire("worktree-git.js")),
7577
+ }));
7578
+ },
7579
+ });
7580
+ pi.registerCommand("codegraph-sync", {
7581
+ description: "Sync this session's checkout CodeGraph index: codegraph sync .",
7582
+ handler: async (args, ctx) => {
7583
+ await run(ctx, () => libRequire("codegraph-commands.js").codegraphSync(args, ctx, {
7584
+ run: libRunner(libRequire("worktree-git.js")),
7585
+ }));
7586
+ },
7587
+ });
7588
+ }
7589
+
7531
7590
  export default function (pi) {
7532
7591
  // A TOOL context has no session control, so `worktree_warp` queues the
7533
7592
  // `/warp` COMMAND through this handle (`sendUserMessage` with
@@ -7537,6 +7596,7 @@ export default function (pi) {
7537
7596
  registerWarpCommand(pi);
7538
7597
  registerOddCommand(pi);
7539
7598
  registerIssuesCommand(pi);
7599
+ registerCodegraphCommands(pi);
7540
7600
  pi.registerTool(worktreeCreateTool);
7541
7601
  pi.registerTool(worktreeListTool);
7542
7602
  pi.registerTool(worktreeCloseTool);
@@ -7560,6 +7620,30 @@ export default function (pi) {
7560
7620
  });
7561
7621
  pi.on("tool_call", (event, ctx) => handleWorktreeOddEvent(event, ctx, pi));
7562
7622
  pi.on("tool_result", (event, ctx) => handleWorktreeOddEvent(event, ctx, pi, true));
7623
+ // S1/S3: a successful edit or write asks for ONE background `codegraph sync`
7624
+ // of that file's checkout. The call is fire-and-forget — never awaited, its
7625
+ // decision never reaches the tool result, and a failure here is swallowed
7626
+ // instead of surfacing as a broken edit.
7627
+ pi.on("tool_result", (event) => {
7628
+ try {
7629
+ if (!event || (event.toolName !== "edit" && event.toolName !== "write")) return;
7630
+ if (event.isError) return;
7631
+ const filePath = event.input && event.input.path;
7632
+ if (typeof filePath !== "string" || filePath.length === 0) return;
7633
+ libRequire("codegraph-session.js").scheduleSync(filePath);
7634
+ } catch {
7635
+ // Index freshness is best effort and never touches the tool result.
7636
+ }
7637
+ });
7638
+ // S7: the ODD worktree rules ride the prompt options, appended after the
7639
+ // harness's own text. Sync mutation, so the host reads the final string.
7640
+ pi.on("before_agent_start", (event) => {
7641
+ try {
7642
+ libRequire("odd-worktrees-prompt.js").appendOddWorktreesSection(event);
7643
+ } catch {
7644
+ // The prompt block is best effort.
7645
+ }
7646
+ });
7563
7647
  // Register every live checkout the registry does not own yet. Dispatched here
7564
7648
  // and awaited by the barrier below: the host runs session_start handlers one
7565
7649
  // at a time, so awaiting the scan inside this handler would put the session's
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+ // The three CodeGraph session commands' bodies (S9): `/codegraph-hook`,
3
+ // `/codegraph-init` and `/codegraph-sync`.
4
+ //
5
+ // They are plain functions returning a render-ready result — `{ok, level,
6
+ // text}` — so the extension only has to hand the line to `ctx.ui.notify`. The
7
+ // registration itself is the extension's job and is gated on the session gate,
8
+ // so a session without the CLI never lists these commands.
9
+ //
10
+ // `/codegraph-hook` flips the in-memory, process-wide switch owned by
11
+ // `codegraph-session.js`: nothing is written to user configuration, and the
12
+ // environment (`CODEGRAPH_HOOK=0`) is only the starting state.
13
+ //
14
+ // `init` and `sync` run the real CLI once, at the session's current directory,
15
+ // through the injected `run` seam. The reported line is the child's real
16
+ // outcome: a non-zero (or unstartable) child is a failure, and no path throws
17
+ // into the command handler.
18
+
19
+ const { isHookEnabled, setHookEnabled } = require('./codegraph-session.js');
20
+
21
+ const CODEGRAPH_BIN = 'codegraph';
22
+ const COMMANDS = { init: 'codegraph init', sync: 'codegraph sync' };
23
+
24
+ function pick(deps, name, fallback) {
25
+ return deps && typeof deps[name] === 'function' ? deps[name] : fallback;
26
+ }
27
+
28
+ // The convention every other lib helper uses for its runner;
29
+ // `runArgvAsync` resolves (never rejects) with `{status, stdout, stderr, error}`.
30
+ function defaultRun(bin, args, cwd) {
31
+ return require('./worktree-git.js').runArgvAsync(bin, args, { cwd });
32
+ }
33
+
34
+ function sessionCwd(ctx) {
35
+ const cwd = ctx && ctx.cwd;
36
+ return typeof cwd === 'string' && cwd.length > 0 ? cwd : process.cwd();
37
+ }
38
+
39
+ // One line of child output, so a multi-line stack never becomes the message.
40
+ function firstLine(text) {
41
+ const raw = String(text === undefined || text === null ? '' : text);
42
+ for (const line of raw.split(/\r?\n/)) {
43
+ const trimmed = line.trim();
44
+ if (trimmed.length > 0) return trimmed.length > 200 ? `${trimmed.slice(0, 200)}…` : trimmed;
45
+ }
46
+ return '';
47
+ }
48
+
49
+ function hookState() {
50
+ return isHookEnabled() ? 'enabled' : 'disabled';
51
+ }
52
+
53
+ // `/codegraph-hook` — no argument toggles; `on` and `off` set the state
54
+ // explicitly; `status` only reports it.
55
+ function codegraphHook(rawArgs) {
56
+ const raw = String(rawArgs === undefined || rawArgs === null ? '' : rawArgs).trim().toLowerCase();
57
+ if (raw === '' || raw === 'on' || raw === 'off') setHookEnabled(raw === '' ? !isHookEnabled() : raw === 'on');
58
+ else if (raw !== 'status') {
59
+ return { ok: false, level: 'error', text: `codegraph hook: unknown option "${raw}". Use on, off or status.` };
60
+ }
61
+ return { ok: true, level: 'info', text: `codegraph hook: ${hookState()}` };
62
+ }
63
+
64
+ // The CLI is chatty and decorative (a box around every line, ANSI colours, and
65
+ // glyphs that change between releases), so the report is built from the plain
66
+ // text it puts on stdout and stderr rather than from the exit status alone. The
67
+ // captured shapes this reads:
68
+ //
69
+ // init, fresh "Initialized in <path>" / "Indexed N files" / "N nodes, M edges in Tms"
70
+ // init, indexed "Already initialized in <path>" / "Use \"codegraph index\" to re-index ..."
71
+ // sync, clean "Already up to date"
72
+ // sync, changed "Synced N changed files" / "Modified: N — M nodes in Tms"
73
+ // sync, no index "CodeGraph not initialized in <path>" (stderr, exit 1)
74
+ const ANSI = /\u001b\[[0-9;]*[a-zA-Z]/g;
75
+
76
+ function plain(text) {
77
+ return String(text === undefined || text === null ? '' : text).replace(ANSI, '');
78
+ }
79
+
80
+ function count(value, noun) {
81
+ const n = Number(value);
82
+ if (!Number.isFinite(n)) return '';
83
+ return `${n} ${noun}${n === 1 ? '' : 's'}`;
84
+ }
85
+
86
+ function summarize(label, out) {
87
+ const text = `${plain(out.stdout)}\n${plain(out.stderr)}`;
88
+ const path = (match) => (match && match[1] ? match[1].trim() : '');
89
+
90
+ const notIndexed = text.match(/not initialized in\s+(\S+)/i);
91
+ if (notIndexed) {
92
+ return { ok: false, level: 'error', text: `${label}: no index in this checkout — run /codegraph-init first` };
93
+ }
94
+ const already = text.match(/already initialized in\s+(\S+)/i);
95
+ if (already) {
96
+ return { ok: true, level: 'info', text: `${label}: already initialized at ${path(already)} — nothing to do, use /codegraph-sync to update` };
97
+ }
98
+ const upToDate = /already up to date/i.test(text);
99
+ if (upToDate) {
100
+ return { ok: true, level: 'info', text: `${label}: already up to date — nothing to sync` };
101
+ }
102
+ const synced = text.match(/synced\s+(\d+)\s+changed\s+files?/i);
103
+ if (synced) {
104
+ const nodes = text.match(/(\d+)\s+nodes?/i);
105
+ const counts = [count(synced[1], 'changed file'), nodes ? count(nodes[1], 'node') : ''].filter(Boolean).join(' — ');
106
+ return { ok: true, level: 'info', text: `${label}: synced ${counts}` };
107
+ }
108
+ const initialized = text.match(/initialized in\s+(\S+)/i);
109
+ if (initialized) {
110
+ const files = text.match(/indexed\s+(\d+)\s+files?/i);
111
+ const nodes = text.match(/(\d+)\s+nodes?/i);
112
+ const edges = text.match(/(\d+)\s+edges?/i);
113
+ const sorted = [
114
+ files ? count(files[1], 'file') : '',
115
+ nodes ? count(nodes[1], 'node') : '',
116
+ edges ? count(edges[1], 'edge') : '',
117
+ ].filter(Boolean).join(', ');
118
+ return { ok: true, level: 'info', text: `${label}: initialized ${path(initialized)}${sorted ? ` — ${sorted}` : ''}` };
119
+ }
120
+ return null;
121
+ }
122
+
123
+ async function runStep(verb, rawArgs, ctx, deps) {
124
+ const label = COMMANDS[verb];
125
+ const raw = String(rawArgs === undefined || rawArgs === null ? '' : rawArgs).trim();
126
+ if (raw !== '') return { ok: false, level: 'error', text: `${label}: takes no argument.` };
127
+
128
+ const cwd = sessionCwd(ctx);
129
+ let outcome;
130
+ try {
131
+ outcome = await pick(deps, 'run', defaultRun)(CODEGRAPH_BIN, [verb, '.'], cwd);
132
+ } catch (err) {
133
+ return { ok: false, level: 'error', text: `${label}: failed — ${firstLine((err && err.message) || String(err))}` };
134
+ }
135
+ const out = outcome && typeof outcome === 'object' ? outcome : {};
136
+ const summary = summarize(label, out);
137
+ // The CLI answers `sync` in a checkout with no index on stderr with exit 1,
138
+ // so the recognised message decides before the exit status does.
139
+ if (summary && (summary.ok || /no index/.test(summary.text))) return summary;
140
+ if (out.error) {
141
+ return { ok: false, level: 'error', text: `${label}: failed — ${firstLine(out.error) || 'unknown error'}` };
142
+ }
143
+ if (out.status !== 0) {
144
+ const exit = Number.isInteger(out.status) ? out.status : 'unknown';
145
+ const detail = firstLine(plain(out.stderr)) || firstLine(plain(out.stdout)) || 'no output';
146
+ return { ok: false, level: 'error', text: `${label}: exit ${exit} — ${detail}` };
147
+ }
148
+ return summary || { ok: true, level: 'info', text: `${label}: done` };
149
+ }
150
+
151
+ // `/codegraph-init` and `/codegraph-sync` — one awaited CLI run at the session's
152
+ // current directory, reported by its real exit status.
153
+ function codegraphInit(rawArgs, ctx, deps) {
154
+ return runStep('init', rawArgs, ctx, deps);
155
+ }
156
+
157
+ function codegraphSync(rawArgs, ctx, deps) {
158
+ return runStep('sync', rawArgs, ctx, deps);
159
+ }
160
+
161
+ module.exports = {
162
+ codegraphHook,
163
+ codegraphInit,
164
+ codegraphSync,
165
+ };