@saifymatteo/pi-extensions 0.0.0-stage → 0.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.
Files changed (183) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE.md +20 -0
  3. package/README.md +151 -2
  4. package/inventory.json +73 -0
  5. package/package.json +74 -4
  6. package/packages/pi-bg-bash/LICENSE +661 -0
  7. package/packages/pi-bg-bash/NOTICE.md +3 -0
  8. package/packages/pi-bg-bash/README.md +41 -0
  9. package/packages/pi-bg-bash/examples/settings.example.json +6 -0
  10. package/packages/pi-bg-bash/package.json +38 -0
  11. package/packages/pi-bg-bash/src/index.ts +969 -0
  12. package/packages/pi-bg-bash/src/lib/async-work.ts +27 -0
  13. package/packages/pi-bg-bash/test/bg-bash.test.mjs +34 -0
  14. package/packages/pi-claude-usage/LICENSE +661 -0
  15. package/packages/pi-claude-usage/NOTICE.md +3 -0
  16. package/packages/pi-claude-usage/README.md +27 -0
  17. package/packages/pi-claude-usage/package.json +35 -0
  18. package/packages/pi-claude-usage/src/index.ts +130 -0
  19. package/packages/pi-clipboard-image/LICENSE +661 -0
  20. package/packages/pi-clipboard-image/NOTICE.md +3 -0
  21. package/packages/pi-clipboard-image/README.md +27 -0
  22. package/packages/pi-clipboard-image/package.json +36 -0
  23. package/packages/pi-clipboard-image/src/index.ts +527 -0
  24. package/packages/pi-codeblock-borders/LICENSE +661 -0
  25. package/packages/pi-codeblock-borders/NOTICE.md +3 -0
  26. package/packages/pi-codeblock-borders/README.md +30 -0
  27. package/packages/pi-codeblock-borders/package.json +35 -0
  28. package/packages/pi-codeblock-borders/src/index.ts +302 -0
  29. package/packages/pi-codegraph/LICENSE +661 -0
  30. package/packages/pi-codegraph/NOTICE.md +5 -0
  31. package/packages/pi-codegraph/README.md +35 -0
  32. package/packages/pi-codegraph/examples/codegraph.example.json +4 -0
  33. package/packages/pi-codegraph/package.json +37 -0
  34. package/packages/pi-codegraph/src/index.ts +573 -0
  35. package/packages/pi-codegraph/test/codegraph.test.mjs +21 -0
  36. package/packages/pi-compact-tools/LICENSE +661 -0
  37. package/packages/pi-compact-tools/NOTICE.md +3 -0
  38. package/packages/pi-compact-tools/README.md +27 -0
  39. package/packages/pi-compact-tools/package.json +36 -0
  40. package/packages/pi-compact-tools/src/index.ts +1161 -0
  41. package/packages/pi-decisions/LICENSE +661 -0
  42. package/packages/pi-decisions/NOTICE.md +3 -0
  43. package/packages/pi-decisions/README.md +37 -0
  44. package/packages/pi-decisions/examples/service-models.example.json +27 -0
  45. package/packages/pi-decisions/package.json +37 -0
  46. package/packages/pi-decisions/src/index.ts +364 -0
  47. package/packages/pi-decisions/src/lib/service-models.ts +95 -0
  48. package/packages/pi-decisions/test/service-models.test.mjs +71 -0
  49. package/packages/pi-load-pi-dotenv/LICENSE +661 -0
  50. package/packages/pi-load-pi-dotenv/NOTICE.md +3 -0
  51. package/packages/pi-load-pi-dotenv/README.md +27 -0
  52. package/packages/pi-load-pi-dotenv/package.json +32 -0
  53. package/packages/pi-load-pi-dotenv/src/index.ts +66 -0
  54. package/packages/pi-model-go/LICENSE +661 -0
  55. package/packages/pi-model-go/NOTICE.md +5 -0
  56. package/packages/pi-model-go/README.md +33 -0
  57. package/packages/pi-model-go/package.json +36 -0
  58. package/packages/pi-model-go/src/index.ts +171 -0
  59. package/packages/pi-model-go/src/lib/dialog.ts +31 -0
  60. package/packages/pi-model-go/src/lib/model-go.ts +239 -0
  61. package/packages/pi-model-go/test/behavior.test.mjs +17 -0
  62. package/packages/pi-prestart-working/LICENSE +661 -0
  63. package/packages/pi-prestart-working/NOTICE.md +3 -0
  64. package/packages/pi-prestart-working/README.md +25 -0
  65. package/packages/pi-prestart-working/package.json +35 -0
  66. package/packages/pi-prestart-working/src/index.ts +86 -0
  67. package/packages/pi-prestart-working/test/behavior.test.mjs +56 -0
  68. package/packages/pi-root-guard/LICENSE +661 -0
  69. package/packages/pi-root-guard/NOTICE.md +3 -0
  70. package/packages/pi-root-guard/README.md +27 -0
  71. package/packages/pi-root-guard/package.json +32 -0
  72. package/packages/pi-root-guard/src/index.ts +202 -0
  73. package/packages/pi-session-finder/LICENSE +661 -0
  74. package/packages/pi-session-finder/NOTICE.md +3 -0
  75. package/packages/pi-session-finder/README.md +33 -0
  76. package/packages/pi-session-finder/package.json +36 -0
  77. package/packages/pi-session-finder/src/index.ts +306 -0
  78. package/packages/pi-session-finder/src/lib/dialog.ts +31 -0
  79. package/packages/pi-session-finder/test/behavior.test.mjs +18 -0
  80. package/packages/pi-session-id/LICENSE +661 -0
  81. package/packages/pi-session-id/NOTICE.md +3 -0
  82. package/packages/pi-session-id/README.md +31 -0
  83. package/packages/pi-session-id/package.json +35 -0
  84. package/packages/pi-session-id/src/index.ts +20 -0
  85. package/packages/pi-session-id/test/behavior.test.mjs +25 -0
  86. package/packages/pi-session-nav/LICENSE +661 -0
  87. package/packages/pi-session-nav/NOTICE.md +3 -0
  88. package/packages/pi-session-nav/README.md +32 -0
  89. package/packages/pi-session-nav/package.json +36 -0
  90. package/packages/pi-session-nav/src/index.ts +203 -0
  91. package/packages/pi-session-nav/src/lib/dialog.ts +31 -0
  92. package/packages/pi-session-nav/test/behavior.test.mjs +36 -0
  93. package/packages/pi-session-rename/LICENSE +661 -0
  94. package/packages/pi-session-rename/NOTICE.md +3 -0
  95. package/packages/pi-session-rename/README.md +31 -0
  96. package/packages/pi-session-rename/package.json +35 -0
  97. package/packages/pi-session-rename/src/index.ts +456 -0
  98. package/packages/pi-session-rename/test/behavior.test.mjs +10 -0
  99. package/packages/pi-session-retention/LICENSE +661 -0
  100. package/packages/pi-session-retention/NOTICE.md +3 -0
  101. package/packages/pi-session-retention/README.md +62 -0
  102. package/packages/pi-session-retention/package.json +36 -0
  103. package/packages/pi-session-retention/src/README.md +42 -0
  104. package/packages/pi-session-retention/src/core.ts +333 -0
  105. package/packages/pi-session-retention/src/dialog.ts +118 -0
  106. package/packages/pi-session-retention/src/index.ts +311 -0
  107. package/packages/pi-session-retention/src/lib/dialog.ts +31 -0
  108. package/packages/pi-session-retention/src/safety.ts +50 -0
  109. package/packages/pi-session-retention/test/behavior.test.mjs +42 -0
  110. package/packages/pi-skill-usage-log/LICENSE +661 -0
  111. package/packages/pi-skill-usage-log/NOTICE.md +3 -0
  112. package/packages/pi-skill-usage-log/README.md +32 -0
  113. package/packages/pi-skill-usage-log/package.json +36 -0
  114. package/packages/pi-skill-usage-log/src/index.ts +316 -0
  115. package/packages/pi-skill-usage-log/src/lib/dialog.ts +31 -0
  116. package/packages/pi-skill-usage-log/src/lib/usage-log.ts +62 -0
  117. package/packages/pi-skill-usage-log/test/behavior.test.mjs +34 -0
  118. package/packages/pi-tool-usage-log/LICENSE +661 -0
  119. package/packages/pi-tool-usage-log/NOTICE.md +5 -0
  120. package/packages/pi-tool-usage-log/README.md +32 -0
  121. package/packages/pi-tool-usage-log/package.json +36 -0
  122. package/packages/pi-tool-usage-log/src/index.ts +233 -0
  123. package/packages/pi-tool-usage-log/src/lib/dialog.ts +31 -0
  124. package/packages/pi-tool-usage-log/src/lib/usage-log.ts +62 -0
  125. package/packages/pi-tool-usage-log/src/skill-usage-log.ts +316 -0
  126. package/packages/pi-tool-usage-log/test/behavior.test.mjs +23 -0
  127. package/packages/pi-tts/LICENSE +661 -0
  128. package/packages/pi-tts/NOTICE.md +3 -0
  129. package/packages/pi-tts/README.md +37 -0
  130. package/packages/pi-tts/examples/service-models.example.json +27 -0
  131. package/packages/pi-tts/package.json +37 -0
  132. package/packages/pi-tts/src/index.ts +135 -0
  133. package/packages/pi-tts/src/lib/service-models.ts +95 -0
  134. package/packages/pi-tts/test/tts-output.test.mjs +47 -0
  135. package/packages/pi-tts/test/tts.test.mjs +57 -0
  136. package/packages/pi-user-image-previews/LICENSE +661 -0
  137. package/packages/pi-user-image-previews/NOTICE.md +3 -0
  138. package/packages/pi-user-image-previews/README.md +27 -0
  139. package/packages/pi-user-image-previews/package.json +36 -0
  140. package/packages/pi-user-image-previews/src/index.ts +168 -0
  141. package/packages/pi-vision/LICENSE +661 -0
  142. package/packages/pi-vision/NOTICE.md +5 -0
  143. package/packages/pi-vision/README.md +43 -0
  144. package/packages/pi-vision/examples/vision-toolkit.example.json +12 -0
  145. package/packages/pi-vision/package.json +38 -0
  146. package/packages/pi-vision/skills/vision-tools/LICENSE +21 -0
  147. package/packages/pi-vision/skills/vision-tools/SKILL.md +134 -0
  148. package/packages/pi-vision/skills/vision-tools/agents/openai.yaml +4 -0
  149. package/packages/pi-vision/skills/vision-tools/bin/crop +94 -0
  150. package/packages/pi-vision/skills/vision-tools/bin/detect +18 -0
  151. package/packages/pi-vision/skills/vision-tools/bin/glance +98 -0
  152. package/packages/pi-vision/skills/vision-tools/bin/ground +18 -0
  153. package/packages/pi-vision/skills/vision-tools/bin/trace +134 -0
  154. package/packages/pi-vision/skills/vision-tools/bin/vision-doctor +168 -0
  155. package/packages/pi-vision/skills/vision-tools/detect.py +56 -0
  156. package/packages/pi-vision/skills/vision-tools/ground.py +216 -0
  157. package/packages/pi-vision/skills/vision-tools/references/gui.md +93 -0
  158. package/packages/pi-vision/skills/vision-tools/references/long-screenshot-ocr.md +103 -0
  159. package/packages/pi-vision/skills/vision-tools/references/restore-graphic.md +92 -0
  160. package/packages/pi-vision/skills/vision-tools/references/restore-structure.md +47 -0
  161. package/packages/pi-vision/skills/vision-tools/references/restore-ui.md +200 -0
  162. package/packages/pi-vision/skills/vision-tools/scripts/dominant_colors.py +224 -0
  163. package/packages/pi-vision/skills/vision-tools/scripts/extract_fg.py +278 -0
  164. package/packages/pi-vision/skills/vision-tools/scripts/html_shot.py +111 -0
  165. package/packages/pi-vision/skills/vision-tools/scripts/long_screenshot_ocr.py +1245 -0
  166. package/packages/pi-vision/skills/vision-tools/scripts/pixel_diff.py +88 -0
  167. package/packages/pi-vision/skills/vision-tools/vision_client.py +581 -0
  168. package/packages/pi-vision/src/index.ts +728 -0
  169. package/packages/pi-vision/test/vision.test.mjs +30 -0
  170. package/packages/pi-wait/LICENSE +661 -0
  171. package/packages/pi-wait/NOTICE.md +3 -0
  172. package/packages/pi-wait/README.md +37 -0
  173. package/packages/pi-wait/package.json +37 -0
  174. package/packages/pi-wait/src/index.ts +88 -0
  175. package/packages/pi-wait/src/lib/async-work.ts +27 -0
  176. package/packages/pi-wait/test/wait.test.mjs +43 -0
  177. package/packages/pi-zvec/LICENSE +661 -0
  178. package/packages/pi-zvec/NOTICE.md +3 -0
  179. package/packages/pi-zvec/README.md +37 -0
  180. package/packages/pi-zvec/examples/zvec.example.json +5 -0
  181. package/packages/pi-zvec/package.json +37 -0
  182. package/packages/pi-zvec/src/index.ts +754 -0
  183. package/packages/pi-zvec/test/zvec.test.mjs +19 -0
@@ -0,0 +1,35 @@
1
+ # @saifymatteo/pi-codegraph
2
+
3
+ ## Purpose and registrations
4
+
5
+ Registers `codegraph_explore`, `/codegraph-init`, `/codegraph-sync`, `/codegraph-status`, `/codegraph-unlock`, session startup/shutdown and settle-sync hooks, and footer status.
6
+
7
+ ## Configuration and external CLI
8
+
9
+ Requires `codegraph` on PATH (external package `@colbymchenry/codegraph`; not installed here). Windows invokes its cmd shim. Reads `<agent-dir>/extensions/codegraph.json`, then index-owner `.pi/codegraph.json`. Boolean keys: `autoInit`, `syncOnSettle`. Missing config is auto-created with both **true**, preserving source behavior. The manually authored example sets both false for cautious testing. Existing indexed roots still receive startup sync in UI modes even when both flags are false.
10
+
11
+ ## Host limitations and security
12
+
13
+ Requires host execution, status and lifecycle APIs. Background indexing runs only with `ctx.hasUI` (TUI/RPC); manual commands still work in print/JSON. Walks parents for `.codegraph/codegraph.db`. The CLI reads source and writes its index; consult its own configuration for model/network behavior. Query results become model-visible. Startup can create agent config and indexes; use an isolated agent directory and disposable workspace. Do not load alongside upstream `@izhimu/pi-codegraph` or another copy of this adapter. Upstream attribution is retained, but the merged package is not claimed to be MIT; see NOTICE.
14
+
15
+ ## Tests
16
+
17
+ From the monorepo root with its existing test dependencies available:
18
+
19
+ ```sh
20
+ node --test packages/pi-codegraph/test/*.test.mjs
21
+ ```
22
+
23
+ Tests use the shared test-only TypeScript/VM loader, fixtures, and host/service stubs. They do not prove compatibility with a live host, CLI, or model service.
24
+
25
+ ## Running locally
26
+
27
+ From the monorepo root:
28
+
29
+ ```sh
30
+ pi -e ./packages/pi-codegraph
31
+ ```
32
+
33
+ Node >=24; host imports are peers, not bundled dependencies. Load only one copy: for an isolated session use `--no-extensions` with explicit `-e` paths so the original live extension is not also discovered. Set `PI_CODING_AGENT_DIR` to a disposable agent directory before launch; unset means the normal `~/.pi/agent` fallback. Extension config continues to resolve there, never relative to this package.
34
+
35
+ Examples are manually authored, sanitized reference documents, not runtime defaults. Review and manually create only the needed files in the isolated agent directory; do not copy live credentials or endpoints. No example is automatically loaded or copied. Loading an extension grants Pi-process OS permissions. Licensed AGPL-3.0-only; see [NOTICE.md](NOTICE.md).
@@ -0,0 +1,4 @@
1
+ {
2
+ "autoInit": false,
3
+ "syncOnSettle": false
4
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@saifymatteo/pi-codegraph",
3
+ "version": "0.1.0",
4
+ "license": "AGPL-3.0-only",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "files": [
10
+ "src",
11
+ "examples",
12
+ "README.md",
13
+ "NOTICE.md",
14
+ "LICENSE"
15
+ ],
16
+ "pi": {
17
+ "extensions": [
18
+ "./src/index.ts"
19
+ ]
20
+ },
21
+ "peerDependencies": {
22
+ "@earendil-works/pi-coding-agent": "*",
23
+ "typebox": "*"
24
+ },
25
+ "engines": {
26
+ "node": ">=24"
27
+ },
28
+ "description": "CodeGraph CLI adapter: explore tool plus index auto-init and sync.",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/saifymatteo/pi-extensions.git",
35
+ "directory": "packages/pi-codegraph"
36
+ }
37
+ }
@@ -0,0 +1,573 @@
1
+ // codegraph — CodeGraph adapter for pi
2
+ //
3
+ // Vendored merge of @izhimu/pi-codegraph 0.3.0 (MIT, https://github.com/izhimu/pi-codegraph)
4
+ // and the former local codegraph-auto.ts automation layer. Upstream is itself an
5
+ // adapter for the codegraph CLI (npm i -g @colbymchenry/codegraph; CLI source and
6
+ // command surface: https://github.com/colbymchenry/codegraph — see
7
+ // research/codegraph-cli.md in this repo for the researched CLI contract).
8
+ //
9
+ // REMOVE @izhimu/pi-codegraph before using this file: it registers the same
10
+ // tool and commands, and a duplicate registration is at best noise.
11
+ //
12
+ // Provides:
13
+ // - codegraph_explore tool : symbol source + call paths from the .codegraph/ index
14
+ // - /codegraph-init /codegraph-sync /codegraph-status /codegraph-unlock
15
+ // - session_start : CLI check (report once if missing), config setup, auto-init
16
+ // when no index is found walking up from cwd (bounded by home
17
+ // dir / filesystem root), and startup sync when indexed
18
+ // - agent_settled : single-flight sync after each completed turn (at most one
19
+ // queued trailing sync)
20
+ // - Footer status : "CodeGraph: init | sync | synced HH:MM | failed" — empty
21
+ // whenever inactive
22
+ // - Errors : background init/sync notify only after retries are exhausted;
23
+ // manual errors and missing CLI notify immediately. Never saved
24
+ // to session history or added to model context
25
+ // - Retries : background init/sync retries after 5s, 15s, 30s; then
26
+ // paused until a successful manual init/sync or new session
27
+ //
28
+ // Config (merged, later wins; both default true):
29
+ // 1. Global : $PI_CODING_AGENT_DIR/extensions/codegraph.json (fallback ~/.pi/agent/extensions/codegraph.json)
30
+ // — auto-created with defaults when missing.
31
+ // 2. Project: <project-root>/.pi/codegraph.json — opt-in per-project overrides.
32
+ // Keys: { "autoInit": boolean, "syncOnSettle": boolean }
33
+ // Edits are picked up on /reload or the next session start.
34
+ //
35
+ // Failure detection: non-zero exit, timeout, or output matching
36
+ // /✗|not initialized|stale lock|already locked|lock file/ — research says all
37
+ // CLI failures exit 1 (research/codegraph-cli.md §2), so exit codes are the
38
+ // primary signal and text matching is defensive belt-and-braces; the fs
39
+ // walk-up check (.codegraph/codegraph.db) decides indexing state.
40
+ //
41
+ // Notes:
42
+ // - Background ops (auto-init, startup sync, settle sync) only spawn when
43
+ // ctx.hasUI (TUI/RPC). In print/JSON mode pi can exit mid-write and leave
44
+ // a stale lock (that is what /codegraph-unlock is for). Config setup and
45
+ // command-driven ops still happen in all modes.
46
+ // - The startup sync targets the index owner found by walking up from cwd,
47
+ // not cwd itself, so subdirectories of an indexed repo sync the right db.
48
+ // - Auto-init is skipped when cwd IS the home dir or a filesystem root
49
+ // (mirrors the CLI's own guard). Sync is allowed even if the index lives
50
+ // in the home dir; disable via config if unwanted.
51
+
52
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
53
+ import { Type } from "typebox";
54
+ import { existsSync, realpathSync } from "node:fs";
55
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
56
+ import { homedir } from "node:os";
57
+ import * as path from "node:path";
58
+
59
+ const INSTALL_HINT =
60
+ "codegraph CLI not found on PATH. Install: npm i -g @colbymchenry/codegraph";
61
+
62
+ const STATUS_KEY = "codegraph";
63
+ const CLI_CHECK_TIMEOUT_MS = 10_000;
64
+ const RETRY_DELAYS_MS = [5_000, 15_000, 30_000];
65
+ const EXPLORE_TIMEOUT_MS = 120_000;
66
+ const SYNC_TIMEOUT_MS = 5 * 60_000; // matches /codegraph-sync
67
+ const INIT_TIMEOUT_MS = 30 * 60_000; // matches /codegraph-init
68
+ const STATUS_TIMEOUT_MS = 30_000;
69
+ const UNLOCK_TIMEOUT_MS = 10_000;
70
+ const FAILURE_RE = /✗|not initialized|stale lock|already locked|lock file/i;
71
+
72
+ interface CgConfig {
73
+ autoInit: boolean;
74
+ syncOnSettle: boolean;
75
+ }
76
+
77
+ interface ProjectState {
78
+ config: CgConfig;
79
+ projectRoot: string;
80
+ indexed: boolean;
81
+ }
82
+
83
+ const DEFAULTS: CgConfig = { autoInit: true, syncOnSettle: true };
84
+
85
+ // Session-scoped module state. The factory re-runs (same module object) when
86
+ // pi rebinds extensions for /new, /resume, /fork and /reload, so a generation
87
+ // token invalidates UI writes from ops belonging to a replaced session.
88
+ let sessionGen = 0;
89
+ let opSeq = 0;
90
+ let currentOpToken = 0; // 0 = idle
91
+ let pendingSync = false;
92
+ let retryTimer: ReturnType<typeof setTimeout> | null = null;
93
+ let retriesExhausted = false;
94
+ let state: ProjectState | null = null;
95
+ let latestCtx: ExtensionContext | null = null;
96
+ let cliAvailable: boolean | null = null;
97
+ let notifiedMissing = false;
98
+ let runtimeAbort = new AbortController();
99
+
100
+ function norm(p: string): string {
101
+ return process.platform === "win32" ? p.toLowerCase() : p;
102
+ }
103
+
104
+ function realSafe(p: string): string {
105
+ try {
106
+ return realpathSync(p);
107
+ } catch {
108
+ return p;
109
+ }
110
+ }
111
+
112
+ function globalConfigPath(): string {
113
+ const dir =
114
+ process.env.PI_CODING_AGENT_DIR || path.join(homedir(), ".pi", "agent");
115
+ return path.join(dir, "extensions", "codegraph.json");
116
+ }
117
+
118
+ async function readConfigFile(p: string): Promise<Partial<CgConfig>> {
119
+ try {
120
+ const parsed = JSON.parse(await readFile(p, "utf8"));
121
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
122
+ return parsed as Partial<CgConfig>;
123
+ }
124
+ } catch {
125
+ // missing or invalid file → treated as {}
126
+ }
127
+ return {};
128
+ }
129
+
130
+ async function ensureGlobalConfig(): Promise<void> {
131
+ const p = globalConfigPath();
132
+ if (existsSync(p)) return;
133
+ try {
134
+ await mkdir(path.dirname(p), { recursive: true });
135
+ await writeFile(p, `${JSON.stringify(DEFAULTS, null, 2)}\n`, "utf8");
136
+ } catch {
137
+ // read-only dir etc. → defaults stay in memory only
138
+ }
139
+ }
140
+
141
+ // Per-key override, only when the value is a proper boolean.
142
+ function boolOr(
143
+ project: Partial<CgConfig>,
144
+ global: Partial<CgConfig>,
145
+ key: keyof CgConfig,
146
+ ): boolean {
147
+ if (typeof project[key] === "boolean") return project[key] as boolean;
148
+ if (typeof global[key] === "boolean") return global[key] as boolean;
149
+ return DEFAULTS[key];
150
+ }
151
+
152
+ // Walk up from startDir looking for .codegraph/codegraph.db. Checks the home
153
+ // dir itself, then stops (nothing above it). Returns the dir owning the index.
154
+ function findIndexRoot(startDir: string): string | null {
155
+ let dir = realSafe(startDir);
156
+ const home = realSafe(homedir());
157
+ for (;;) {
158
+ if (existsSync(path.join(dir, ".codegraph", "codegraph.db"))) return dir;
159
+ const parent = path.dirname(dir);
160
+ if (parent === dir) return null; // filesystem root checked, stop
161
+ if (norm(dir) === norm(home)) return null; // home checked, stop
162
+ dir = parent;
163
+ }
164
+ }
165
+
166
+ // True when cwd IS the home dir or a filesystem root (CLI's init guard).
167
+ function isHomeOrRoot(dir: string): boolean {
168
+ const real = realSafe(dir);
169
+ if (path.dirname(real) === real) return true;
170
+ return norm(real) === norm(realSafe(homedir()));
171
+ }
172
+
173
+ function combined(r: { stdout: string; stderr: string }): string {
174
+ return `${r.stdout}\n${r.stderr}`.trim();
175
+ }
176
+
177
+ function outputOf(result: { stdout: string; stderr: string; code: number }): string {
178
+ return combined(result) || `exit ${result.code}`;
179
+ }
180
+
181
+ function hhmm(): string {
182
+ const d = new Date();
183
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
184
+ }
185
+
186
+ function textResult(text: string) {
187
+ return { content: [{ type: "text" as const, text }], details: undefined };
188
+ }
189
+
190
+ async function execCg(
191
+ pi: ExtensionAPI,
192
+ args: string[],
193
+ options: { signal?: AbortSignal; timeout?: number; cwd?: string } = {},
194
+ ) {
195
+ if (process.platform === "win32") {
196
+ return await pi.exec("cmd.exe", ["/d", "/s", "/c", "codegraph", ...args], options);
197
+ }
198
+ return await pi.exec("codegraph", args, options);
199
+ }
200
+
201
+ export default function codegraph(pi: ExtensionAPI) {
202
+ // ── shared CLI availability check ─────────────────────────────────────
203
+ async function ensureCli(pi2: ExtensionAPI): Promise<boolean> {
204
+ if (cliAvailable !== null) return cliAvailable;
205
+ try {
206
+ const r = await execCg(pi2, ["version"], { timeout: CLI_CHECK_TIMEOUT_MS });
207
+ cliAvailable = r.code === 0;
208
+ } catch {
209
+ cliAvailable = false;
210
+ }
211
+ return cliAvailable;
212
+ }
213
+
214
+ // ── UI helpers: drop writes from a replaced session, never crash pi ───
215
+ function status(ctx: ExtensionContext, gen: number, text: string | undefined) {
216
+ if (gen !== sessionGen) return;
217
+ try {
218
+ ctx.ui.setStatus(STATUS_KEY, text);
219
+ } catch {
220
+ /* replaced session */
221
+ }
222
+ }
223
+
224
+ function reportError(ctx: ExtensionContext, gen: number, title: string, detail = "") {
225
+ if (gen !== sessionGen || !ctx.hasUI) return;
226
+ try {
227
+ ctx.ui.notify(detail ? `${title}\n${detail}` : title, "error");
228
+ } catch {
229
+ /* replaced session */
230
+ }
231
+ }
232
+
233
+ // A waiting retry owns the single-flight slot, just like a running child.
234
+ function cancelRetry() {
235
+ if (retryTimer === null) return;
236
+ clearTimeout(retryTimer);
237
+ retryTimer = null;
238
+ currentOpToken = 0;
239
+ pendingSync = false;
240
+ }
241
+
242
+ // ── background ops (init/sync): bounded retries + trailing sync ─────────
243
+ // Spawn init/sync, report outcome, then run a queued trailing sync if any.
244
+ function spawnOp(kind: "init" | "sync", ctx: ExtensionContext, gen: number, target: string, attempt = 0) {
245
+ const token = ++opSeq;
246
+ currentOpToken = token;
247
+ const args =
248
+ kind === "init"
249
+ ? ["--no-color", "init", "-y", target]
250
+ : ["--no-color", "sync", "-q", target];
251
+ const timeout = kind === "init" ? INIT_TIMEOUT_MS : SYNC_TIMEOUT_MS;
252
+ status(ctx, gen, kind === "init" ? "CodeGraph: init" : "CodeGraph: sync");
253
+
254
+ void (async () => {
255
+ let failure: string | null = null;
256
+ let detail = "";
257
+ try {
258
+ const r = await execCg(pi, args, { timeout, cwd: target, signal: runtimeAbort.signal });
259
+ const out = combined(r);
260
+ if (r.killed) {
261
+ failure = `CodeGraph ${kind} timed out (${Math.round(timeout / 60_000)}m)`;
262
+ detail = out;
263
+ } else if (r.code !== 0 || FAILURE_RE.test(out)) {
264
+ failure = `CodeGraph ${kind} failed (exit ${r.code})`;
265
+ detail = out || "(no output)";
266
+ }
267
+ } catch (err) {
268
+ failure = `CodeGraph ${kind} failed`;
269
+ detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
270
+ }
271
+
272
+ if (gen !== sessionGen || currentOpToken !== token) return;
273
+ if (failure) {
274
+ const delay = RETRY_DELAYS_MS[attempt];
275
+ if (delay !== undefined) {
276
+ status(ctx, gen, `CodeGraph: retry ${attempt + 1}/3 in ${delay / 1000}s`);
277
+ retryTimer = setTimeout(() => {
278
+ if (gen !== sessionGen || currentOpToken !== token) return;
279
+ retryTimer = null;
280
+ spawnOp(kind, ctx, gen, target, attempt + 1);
281
+ }, delay);
282
+ return;
283
+ }
284
+ // Recovered retries leave no notification; report only the final failure.
285
+ reportError(ctx, gen, failure, detail);
286
+ retriesExhausted = true;
287
+ pendingSync = false;
288
+ status(ctx, gen, "CodeGraph: failed");
289
+ } else {
290
+ // A successful init or sync of the tracked project root means the
291
+ // index exists — settle syncs may proceed.
292
+ if (state && norm(state.projectRoot) === norm(target)) {
293
+ state.indexed = true;
294
+ }
295
+ status(ctx, gen, `CodeGraph: synced ${hhmm()}`);
296
+ }
297
+ if (currentOpToken === token) currentOpToken = 0;
298
+ maybeTrailing();
299
+ })();
300
+ }
301
+
302
+ function maybeTrailing() {
303
+ if (!pendingSync || currentOpToken !== 0) return;
304
+ pendingSync = false;
305
+ if (retriesExhausted || !state?.indexed || !state.config.syncOnSettle || !latestCtx) return;
306
+ spawnOp("sync", latestCtx, sessionGen, state.projectRoot);
307
+ }
308
+
309
+ // ── codegraph_explore tool ─────────────────────────────────────────────
310
+ pi.registerTool({
311
+ name: "codegraph_explore",
312
+ label: "CodeGraph Explore",
313
+ description:
314
+ "Structural code queries against the project's CodeGraph index: the relevant symbols' verbatim " +
315
+ "line-numbered source and the call paths between them.",
316
+ promptSnippet:
317
+ "Symbol source + call paths from the project's CodeGraph index",
318
+ promptGuidelines: [
319
+ "Use codegraph_explore for structural questions: who calls a symbol, what it calls, what a symbol depends on, the call path between two symbols, or the blast radius of a change.",
320
+ "The index auto-initializes and auto-syncs; /codegraph-init forces a full rebuild.",
321
+ ],
322
+ parameters: Type.Object({
323
+ query: Type.String({
324
+ description: "Symbol names or a natural-language question about the code",
325
+ }),
326
+ path: Type.Optional(
327
+ Type.String({
328
+ description: "Project path to query; defaults to the current working directory",
329
+ }),
330
+ ),
331
+ maxFiles: Type.Optional(
332
+ Type.Integer({
333
+ description: "Maximum number of files to include source from",
334
+ }),
335
+ ),
336
+ }),
337
+ execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
338
+ if (!(await ensureCli(pi))) return textResult(INSTALL_HINT);
339
+ const cwd = params.path ?? ctx.cwd;
340
+ const args = ["explore", params.query, "-p", cwd];
341
+ if (typeof params.maxFiles === "number" && params.maxFiles > 0) {
342
+ args.push("--max-files", String(params.maxFiles));
343
+ }
344
+ const result = await execCg(pi, args, { signal, timeout: EXPLORE_TIMEOUT_MS });
345
+ if (result.killed) return textResult("codegraph explore timed out (120s)");
346
+ // Non-zero exits carry upstream's agent-friendly guidance (e.g. the
347
+ // "not initialized" message) — pass it through verbatim.
348
+ if (result.code !== 0) return textResult(outputOf(result));
349
+ return textResult(result.stdout.trim());
350
+ },
351
+ });
352
+
353
+ // Command exceptions use the same non-persistent notification as CLI failures.
354
+ function registerCommand(name: string, command: Parameters<ExtensionAPI["registerCommand"]>[1]) {
355
+ pi.registerCommand(name, {
356
+ ...command,
357
+ handler: async (args, ctx) => {
358
+ const gen = sessionGen;
359
+ const indexes = name === "codegraph-init" || name === "codegraph-sync";
360
+ // A manual operation can replace a delayed retry, not a live writer.
361
+ if (indexes && currentOpToken !== 0 && retryTimer === null) {
362
+ if (ctx.hasUI) ctx.ui.notify(`CodeGraph: indexing already in progress. Try /${name} again when it finishes.`, "info");
363
+ return;
364
+ }
365
+ if (indexes) cancelRetry();
366
+ const token = indexes ? ++opSeq : 0;
367
+ if (token) currentOpToken = token;
368
+ try {
369
+ await command.handler(args, ctx);
370
+ } catch (err) {
371
+ reportError(ctx, gen, `CodeGraph command /${name} failed`, err instanceof Error ? (err.stack ?? err.message) : String(err));
372
+ } finally {
373
+ if (token && gen === sessionGen && currentOpToken === token) {
374
+ currentOpToken = 0;
375
+ maybeTrailing();
376
+ }
377
+ }
378
+ },
379
+ });
380
+ }
381
+
382
+ // ── /codegraph-init ────────────────────────────────────────────────────
383
+ registerCommand("codegraph-init", {
384
+ description: "Build the CodeGraph index for the current project (codegraph init)",
385
+ handler: async (args, ctx) => {
386
+ const gen = sessionGen;
387
+ if (!(await ensureCli(pi))) {
388
+ reportError(ctx, gen, INSTALL_HINT);
389
+ return;
390
+ }
391
+ if (gen !== sessionGen) return;
392
+ const target = args?.trim() || ctx.cwd;
393
+ if (ctx.hasUI) {
394
+ ctx.ui.notify(`Indexing ${target} — can take minutes on a large repo…`, "info");
395
+ }
396
+ const result = await execCg(pi, ["--no-color", "init", "-y", target], {
397
+ timeout: INIT_TIMEOUT_MS,
398
+ cwd: target,
399
+ });
400
+ const out = outputOf(result);
401
+ if (gen !== sessionGen) return;
402
+ const success = !result.killed && result.code === 0 && !FAILURE_RE.test(out);
403
+ if (!success) {
404
+ reportError(ctx, gen, result.killed ? "CodeGraph init timed out" : `CodeGraph init failed (exit ${result.code})`, out);
405
+ return;
406
+ }
407
+ retriesExhausted = false;
408
+ if (state) {
409
+ state.projectRoot = target;
410
+ state.indexed = true;
411
+ }
412
+ if (ctx.hasUI) ctx.ui.notify("CodeGraph index built.", "info");
413
+ pi.sendMessage(
414
+ { customType: "codegraph-init", content: out, display: true },
415
+ { triggerTurn: false },
416
+ );
417
+ },
418
+ });
419
+
420
+ // ── /codegraph-sync ────────────────────────────────────────────────────
421
+ registerCommand("codegraph-sync", {
422
+ description: "Sync CodeGraph changes since last index (codegraph sync)",
423
+ handler: async (args, ctx) => {
424
+ const gen = sessionGen;
425
+ if (!(await ensureCli(pi))) {
426
+ reportError(ctx, gen, INSTALL_HINT);
427
+ return;
428
+ }
429
+ if (gen !== sessionGen) return;
430
+ const target = args?.trim() || ctx.cwd;
431
+ if (ctx.hasUI) {
432
+ ctx.ui.notify(`Syncing CodeGraph for ${target}…`, "info");
433
+ }
434
+ const result = await execCg(pi, ["--no-color", "sync", target], {
435
+ timeout: SYNC_TIMEOUT_MS,
436
+ cwd: target,
437
+ });
438
+ const out = outputOf(result);
439
+ if (gen !== sessionGen) return;
440
+ const success = !result.killed && result.code === 0 && !FAILURE_RE.test(out);
441
+ if (!success) {
442
+ reportError(ctx, gen, result.killed ? "CodeGraph sync timed out" : `CodeGraph sync failed (exit ${result.code})`, out);
443
+ return;
444
+ }
445
+ retriesExhausted = false;
446
+ if (state) state.indexed = true;
447
+ if (ctx.hasUI) ctx.ui.notify("CodeGraph synced.", "info");
448
+ pi.sendMessage(
449
+ { customType: "codegraph-sync", content: out, display: true },
450
+ { triggerTurn: false },
451
+ );
452
+ },
453
+ });
454
+
455
+ // ── /codegraph-status ──────────────────────────────────────────────────
456
+ registerCommand("codegraph-status", {
457
+ description: "Show CodeGraph index status and statistics",
458
+ handler: async (args, ctx) => {
459
+ const gen = sessionGen;
460
+ if (!(await ensureCli(pi))) {
461
+ reportError(ctx, gen, INSTALL_HINT);
462
+ return;
463
+ }
464
+ if (gen !== sessionGen) return;
465
+ const target = args?.trim() || ctx.cwd;
466
+ const result = await execCg(pi, ["status", target], { timeout: STATUS_TIMEOUT_MS });
467
+ if (gen !== sessionGen) return;
468
+ const out = outputOf(result);
469
+ if (result.code !== 0 || result.killed || /✗|not initialized/i.test(out)) {
470
+ reportError(ctx, gen, result.killed ? "CodeGraph status timed out" : `CodeGraph status failed (exit ${result.code})`, out);
471
+ return;
472
+ }
473
+ if (ctx.hasUI) ctx.ui.notify(out, "info");
474
+ pi.sendMessage(
475
+ { customType: "codegraph-status", content: out, display: true },
476
+ { triggerTurn: false },
477
+ );
478
+ },
479
+ });
480
+
481
+ // ── /codegraph-unlock ──────────────────────────────────────────────────
482
+ registerCommand("codegraph-unlock", {
483
+ description: "Release stale CodeGraph database lock (codegraph unlock)",
484
+ handler: async (args, ctx) => {
485
+ const gen = sessionGen;
486
+ if (!(await ensureCli(pi))) {
487
+ reportError(ctx, gen, INSTALL_HINT);
488
+ return;
489
+ }
490
+ if (gen !== sessionGen) return;
491
+ const target = args?.trim() || ctx.cwd;
492
+ const result = await execCg(pi, ["unlock", target], { timeout: UNLOCK_TIMEOUT_MS });
493
+ if (gen !== sessionGen) return;
494
+ const out = outputOf(result);
495
+ // Successful unlock output can mention the stale lock/lock file it removed.
496
+ if (result.code !== 0 || result.killed || /✗|not initialized/i.test(out)) {
497
+ reportError(ctx, gen, result.killed ? "CodeGraph unlock timed out" : `CodeGraph unlock failed (exit ${result.code})`, out);
498
+ return;
499
+ }
500
+ if (ctx.hasUI) ctx.ui.notify(out, "info");
501
+ pi.sendMessage(
502
+ { customType: "codegraph-unlock", content: out, display: true },
503
+ { triggerTurn: false },
504
+ );
505
+ },
506
+ });
507
+
508
+ pi.on("session_shutdown", () => {
509
+ cancelRetry();
510
+ runtimeAbort.abort();
511
+ sessionGen++;
512
+ currentOpToken = 0;
513
+ pendingSync = false;
514
+ latestCtx = null;
515
+ });
516
+
517
+ // ── session_start: CLI check → config → auto-init / startup sync ──────
518
+ pi.on("session_start", async (_event, ctx) => {
519
+ runtimeAbort.abort();
520
+ runtimeAbort = new AbortController();
521
+ const gen = ++sessionGen;
522
+ cancelRetry();
523
+ retriesExhausted = false;
524
+ state = null;
525
+ latestCtx = ctx;
526
+ currentOpToken = 0; // invalidate any op from a replaced session
527
+ pendingSync = false;
528
+ status(ctx, gen, undefined);
529
+
530
+ if (!(await ensureCli(pi))) {
531
+ if (gen !== sessionGen) return;
532
+ if (ctx.hasUI && !notifiedMissing) {
533
+ notifiedMissing = true;
534
+ reportError(ctx, gen, INSTALL_HINT);
535
+ }
536
+ return;
537
+ }
538
+
539
+ // Config setup happens in every mode; background spawning is TUI/RPC only.
540
+ await ensureGlobalConfig();
541
+ const indexedRoot = findIndexRoot(ctx.cwd);
542
+ const projectRoot = indexedRoot ?? ctx.cwd;
543
+ const globalCfg = await readConfigFile(globalConfigPath());
544
+ const projectCfg = await readConfigFile(path.join(projectRoot, ".pi", "codegraph.json"));
545
+ const config: CgConfig = {
546
+ autoInit: boolOr(projectCfg, globalCfg, "autoInit"),
547
+ syncOnSettle: boolOr(projectCfg, globalCfg, "syncOnSettle"),
548
+ };
549
+ if (gen !== sessionGen) return;
550
+ state = { config, projectRoot, indexed: indexedRoot !== null };
551
+
552
+ if (indexedRoot !== null) {
553
+ // Startup sync on the index owner (not cwd — cwd may be a subdir).
554
+ if (ctx.hasUI) spawnOp("sync", ctx, gen, projectRoot);
555
+ } else if (config.autoInit && ctx.hasUI && !isHomeOrRoot(ctx.cwd)) {
556
+ spawnOp("init", ctx, gen, ctx.cwd);
557
+ }
558
+ });
559
+
560
+ // ── agent_settled: single-flight sync after each completed turn ────────
561
+ pi.on("agent_settled", (_event, ctx) => {
562
+ latestCtx = ctx;
563
+ // Print/JSON mode: pi exits after prompts; a fire-and-forget child
564
+ // could be killed mid-write and leave a stale lock.
565
+ if (!ctx.hasUI) return;
566
+ if (retriesExhausted || !state?.indexed || !state.config.syncOnSettle) return;
567
+ if (currentOpToken !== 0) {
568
+ pendingSync = true; // at most one trailing sync
569
+ return;
570
+ }
571
+ spawnOp("sync", ctx, sessionGen, state.projectRoot);
572
+ });
573
+ }
@@ -0,0 +1,21 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { loadExtension } from '../../../test/load-extension.mjs';
5
+
6
+ test('explore delegates query and source limit to CLI without session-start side effects', async () => {
7
+ let tool;
8
+ const calls = [];
9
+ const Type = new Proxy({}, { get: () => () => ({}) });
10
+ const { default: extension } = loadExtension(fileURLToPath(new URL('../src/index.ts', import.meta.url)), { typebox: { Type } });
11
+ extension({ registerTool: value => { tool = value; }, registerCommand() {}, on() {}, exec: async (command, args, options) => {
12
+ calls.push({ command, args, options });
13
+ return { code: 0, stdout: args.includes('version') ? '1.0' : 'symbol source', stderr: '' };
14
+ } });
15
+ const result = await tool.execute('test', { query: 'findCaller', path: 'fixture-project', maxFiles: 3 }, undefined, undefined, { cwd: 'unused' });
16
+ assert.equal(tool.name, 'codegraph_explore');
17
+ assert.equal(result.content[0].text, 'symbol source');
18
+ assert.equal(calls.length, 2);
19
+ assert.equal(calls[1].args.slice(-5).join('|'), 'findCaller|-p|fixture-project|--max-files|3');
20
+ assert.equal(calls[1].options.timeout, 120000);
21
+ });