@hellcoder/companion 0.96.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 (242) hide show
  1. package/bin/cli.ts +168 -0
  2. package/bin/ctl.ts +528 -0
  3. package/bin/generate-token.ts +28 -0
  4. package/dist/apple-touch-icon.png +0 -0
  5. package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
  6. package/dist/assets/CronManager-EGwLJONv.js +1 -0
  7. package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
  8. package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
  9. package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
  10. package/dist/assets/Playground-BV3k0RbV.js +109 -0
  11. package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
  12. package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
  13. package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
  14. package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
  15. package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
  16. package/dist/assets/index-BhUa1e6X.css +1 -0
  17. package/dist/assets/index-DkqeP-R9.js +134 -0
  18. package/dist/assets/sw-register-BibwRdvC.js +1 -0
  19. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
  20. package/dist/favicon.svg +8 -0
  21. package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
  22. package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
  23. package/dist/icon-192.png +0 -0
  24. package/dist/icon-512.png +0 -0
  25. package/dist/index.html +20 -0
  26. package/dist/logo-codex.svg +14 -0
  27. package/dist/logo-docker.svg +4 -0
  28. package/dist/logo.svg +14 -0
  29. package/dist/manifest.json +24 -0
  30. package/dist/sw.js +2 -0
  31. package/package.json +104 -0
  32. package/server/agent-cron-migrator.test.ts +610 -0
  33. package/server/agent-cron-migrator.ts +85 -0
  34. package/server/agent-executor.test.ts +1108 -0
  35. package/server/agent-executor.ts +346 -0
  36. package/server/agent-store.test.ts +588 -0
  37. package/server/agent-store.ts +185 -0
  38. package/server/agent-types.ts +138 -0
  39. package/server/ai-validation-settings.test.ts +128 -0
  40. package/server/ai-validation-settings.ts +35 -0
  41. package/server/ai-validator.test.ts +387 -0
  42. package/server/ai-validator.ts +271 -0
  43. package/server/auth-manager.test.ts +83 -0
  44. package/server/auth-manager.ts +150 -0
  45. package/server/auto-namer.test.ts +252 -0
  46. package/server/auto-namer.ts +78 -0
  47. package/server/backend-adapter.test.ts +38 -0
  48. package/server/backend-adapter.ts +54 -0
  49. package/server/cache-headers.test.ts +98 -0
  50. package/server/cache-headers.ts +61 -0
  51. package/server/claude-adapter.test.ts +1363 -0
  52. package/server/claude-adapter.ts +889 -0
  53. package/server/claude-container-auth.test.ts +44 -0
  54. package/server/claude-container-auth.ts +30 -0
  55. package/server/claude-protocol-contract.test.ts +71 -0
  56. package/server/claude-protocol-drift.test.ts +78 -0
  57. package/server/claude-session-discovery.test.ts +132 -0
  58. package/server/claude-session-discovery.ts +157 -0
  59. package/server/claude-session-history.test.ts +158 -0
  60. package/server/claude-session-history.ts +410 -0
  61. package/server/cli-launcher.test.ts +1343 -0
  62. package/server/cli-launcher.ts +1298 -0
  63. package/server/cli.test.ts +16 -0
  64. package/server/codex-adapter.test.ts +5545 -0
  65. package/server/codex-adapter.ts +3062 -0
  66. package/server/codex-container-auth.test.ts +50 -0
  67. package/server/codex-container-auth.ts +24 -0
  68. package/server/codex-home.test.ts +61 -0
  69. package/server/codex-home.ts +26 -0
  70. package/server/codex-protocol-contract.test.ts +96 -0
  71. package/server/codex-protocol-drift.test.ts +123 -0
  72. package/server/codex-ws-proxy.cjs +226 -0
  73. package/server/commands-discovery.test.ts +179 -0
  74. package/server/commands-discovery.ts +81 -0
  75. package/server/constants.ts +7 -0
  76. package/server/container-manager.test.ts +1211 -0
  77. package/server/container-manager.ts +1053 -0
  78. package/server/cron-scheduler.test.ts +957 -0
  79. package/server/cron-scheduler.ts +243 -0
  80. package/server/cron-store.test.ts +422 -0
  81. package/server/cron-store.ts +148 -0
  82. package/server/cron-types.ts +63 -0
  83. package/server/env-manager.test.ts +268 -0
  84. package/server/env-manager.ts +161 -0
  85. package/server/event-bus-types.ts +64 -0
  86. package/server/event-bus.test.ts +244 -0
  87. package/server/event-bus.ts +124 -0
  88. package/server/execution-store.test.ts +307 -0
  89. package/server/execution-store.ts +170 -0
  90. package/server/fs-utils.ts +15 -0
  91. package/server/git-utils.test.ts +938 -0
  92. package/server/git-utils.ts +421 -0
  93. package/server/github-pr.test.ts +498 -0
  94. package/server/github-pr.ts +379 -0
  95. package/server/image-pull-manager.test.ts +303 -0
  96. package/server/image-pull-manager.ts +279 -0
  97. package/server/index.ts +396 -0
  98. package/server/linear-agent-bridge.test.ts +1157 -0
  99. package/server/linear-agent-bridge.ts +629 -0
  100. package/server/linear-agent.test.ts +473 -0
  101. package/server/linear-agent.ts +479 -0
  102. package/server/linear-cache.test.ts +136 -0
  103. package/server/linear-cache.ts +113 -0
  104. package/server/linear-connections.test.ts +350 -0
  105. package/server/linear-connections.ts +231 -0
  106. package/server/linear-credential-migration.test.ts +337 -0
  107. package/server/linear-credential-migration.ts +63 -0
  108. package/server/linear-oauth-connections-migration.test.ts +268 -0
  109. package/server/linear-oauth-connections.test.ts +365 -0
  110. package/server/linear-oauth-connections.ts +294 -0
  111. package/server/linear-project-manager.test.ts +162 -0
  112. package/server/linear-project-manager.ts +111 -0
  113. package/server/linear-prompt-builder.test.ts +74 -0
  114. package/server/linear-prompt-builder.ts +61 -0
  115. package/server/linear-staging.test.ts +276 -0
  116. package/server/linear-staging.ts +142 -0
  117. package/server/logger.test.ts +393 -0
  118. package/server/logger.ts +259 -0
  119. package/server/metrics-collector.test.ts +413 -0
  120. package/server/metrics-collector.ts +350 -0
  121. package/server/metrics-types.ts +108 -0
  122. package/server/middleware/managed-auth.test.ts +264 -0
  123. package/server/middleware/managed-auth.ts +195 -0
  124. package/server/novnc-proxy.test.ts +333 -0
  125. package/server/novnc-proxy.ts +99 -0
  126. package/server/path-resolver.test.ts +552 -0
  127. package/server/path-resolver.ts +186 -0
  128. package/server/paths.test.ts +31 -0
  129. package/server/paths.ts +11 -0
  130. package/server/pr-poller.test.ts +191 -0
  131. package/server/pr-poller.ts +162 -0
  132. package/server/prompt-manager.test.ts +211 -0
  133. package/server/prompt-manager.ts +211 -0
  134. package/server/protocol/claude-upstream/README.md +19 -0
  135. package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
  136. package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
  137. package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
  138. package/server/protocol/codex-upstream/README.md +18 -0
  139. package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
  140. package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
  141. package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
  142. package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
  143. package/server/protocol-monitor.ts +50 -0
  144. package/server/recorder.test.ts +454 -0
  145. package/server/recorder.ts +374 -0
  146. package/server/recording-hub/compat-validator.test.ts +150 -0
  147. package/server/recording-hub/compat-validator.ts +284 -0
  148. package/server/recording-hub/diagnostics.test.ts +140 -0
  149. package/server/recording-hub/diagnostics.ts +299 -0
  150. package/server/recording-hub/hub-config.test.ts +44 -0
  151. package/server/recording-hub/hub-config.ts +19 -0
  152. package/server/recording-hub/hub-routes.test.ts +417 -0
  153. package/server/recording-hub/hub-routes.ts +236 -0
  154. package/server/recording-hub/hub-store.test.ts +262 -0
  155. package/server/recording-hub/hub-store.ts +265 -0
  156. package/server/recording-hub/replay-adapter.test.ts +294 -0
  157. package/server/recording-hub/replay-adapter.ts +207 -0
  158. package/server/relay-client.test.ts +337 -0
  159. package/server/relay-client.ts +320 -0
  160. package/server/replay.test.ts +200 -0
  161. package/server/replay.ts +78 -0
  162. package/server/routes/agent-routes.test.ts +1400 -0
  163. package/server/routes/agent-routes.ts +409 -0
  164. package/server/routes/cron-routes.test.ts +881 -0
  165. package/server/routes/cron-routes.ts +103 -0
  166. package/server/routes/env-routes.test.ts +383 -0
  167. package/server/routes/env-routes.ts +95 -0
  168. package/server/routes/fs-routes.test.ts +1198 -0
  169. package/server/routes/fs-routes.ts +605 -0
  170. package/server/routes/git-routes.test.ts +813 -0
  171. package/server/routes/git-routes.ts +97 -0
  172. package/server/routes/linear-agent-routes.test.ts +721 -0
  173. package/server/routes/linear-agent-routes.ts +304 -0
  174. package/server/routes/linear-connection-routes.test.ts +927 -0
  175. package/server/routes/linear-connection-routes.ts +244 -0
  176. package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
  177. package/server/routes/linear-oauth-connection-routes.ts +129 -0
  178. package/server/routes/linear-routes.test.ts +1510 -0
  179. package/server/routes/linear-routes.ts +953 -0
  180. package/server/routes/metrics-routes.test.ts +103 -0
  181. package/server/routes/metrics-routes.ts +13 -0
  182. package/server/routes/prompt-routes.ts +67 -0
  183. package/server/routes/sandbox-routes.test.ts +513 -0
  184. package/server/routes/sandbox-routes.ts +127 -0
  185. package/server/routes/settings-routes.ts +270 -0
  186. package/server/routes/skills-routes.test.ts +690 -0
  187. package/server/routes/skills-routes.ts +100 -0
  188. package/server/routes/system-routes.test.ts +637 -0
  189. package/server/routes/system-routes.ts +228 -0
  190. package/server/routes/tailscale-routes.test.ts +176 -0
  191. package/server/routes/tailscale-routes.ts +22 -0
  192. package/server/routes.test.ts +4655 -0
  193. package/server/routes.ts +1277 -0
  194. package/server/sandbox-manager.test.ts +378 -0
  195. package/server/sandbox-manager.ts +168 -0
  196. package/server/service.test.ts +1419 -0
  197. package/server/service.ts +718 -0
  198. package/server/session-creation-service.test.ts +661 -0
  199. package/server/session-creation-service.ts +473 -0
  200. package/server/session-git-info.ts +104 -0
  201. package/server/session-linear-issues.test.ts +118 -0
  202. package/server/session-linear-issues.ts +88 -0
  203. package/server/session-names.test.ts +94 -0
  204. package/server/session-names.ts +67 -0
  205. package/server/session-orchestrator.test.ts +1784 -0
  206. package/server/session-orchestrator.ts +973 -0
  207. package/server/session-state-machine.test.ts +606 -0
  208. package/server/session-state-machine.ts +207 -0
  209. package/server/session-store.test.ts +290 -0
  210. package/server/session-store.ts +146 -0
  211. package/server/session-types.ts +509 -0
  212. package/server/settings-manager.test.ts +275 -0
  213. package/server/settings-manager.ts +173 -0
  214. package/server/tailscale-manager.test.ts +553 -0
  215. package/server/tailscale-manager.ts +451 -0
  216. package/server/terminal-manager.ts +240 -0
  217. package/server/update-checker.test.ts +306 -0
  218. package/server/update-checker.ts +197 -0
  219. package/server/usage-limits.test.ts +536 -0
  220. package/server/usage-limits.ts +225 -0
  221. package/server/worktree-tracker.test.ts +243 -0
  222. package/server/worktree-tracker.ts +84 -0
  223. package/server/ws-auth.test.ts +59 -0
  224. package/server/ws-auth.ts +41 -0
  225. package/server/ws-bridge-browser-ingest.test.ts +272 -0
  226. package/server/ws-bridge-browser-ingest.ts +72 -0
  227. package/server/ws-bridge-browser.ts +112 -0
  228. package/server/ws-bridge-cli-ingest.test.ts +302 -0
  229. package/server/ws-bridge-cli-ingest.ts +81 -0
  230. package/server/ws-bridge-codex.test.ts +1837 -0
  231. package/server/ws-bridge-codex.ts +266 -0
  232. package/server/ws-bridge-controls.test.ts +124 -0
  233. package/server/ws-bridge-controls.ts +20 -0
  234. package/server/ws-bridge-persist.test.ts +296 -0
  235. package/server/ws-bridge-persist.ts +66 -0
  236. package/server/ws-bridge-publish.test.ts +234 -0
  237. package/server/ws-bridge-publish.ts +79 -0
  238. package/server/ws-bridge-replay.test.ts +44 -0
  239. package/server/ws-bridge-replay.ts +61 -0
  240. package/server/ws-bridge-types.ts +106 -0
  241. package/server/ws-bridge.test.ts +4777 -0
  242. package/server/ws-bridge.ts +1279 -0
@@ -0,0 +1,211 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+
5
+ let tempDir: string;
6
+ let promptManager: typeof import("./prompt-manager.js");
7
+
8
+ const mockHomedir = vi.hoisted(() => {
9
+ let dir = "";
10
+ return {
11
+ get: () => dir,
12
+ set: (d: string) => {
13
+ dir = d;
14
+ },
15
+ };
16
+ });
17
+
18
+ vi.mock("node:os", async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import("node:os")>();
20
+ return {
21
+ ...actual,
22
+ homedir: () => mockHomedir.get(),
23
+ };
24
+ });
25
+
26
+ beforeEach(async () => {
27
+ tempDir = mkdtempSync(join(tmpdir(), "prompt-test-"));
28
+ mockHomedir.set(tempDir);
29
+ vi.resetModules();
30
+ promptManager = await import("./prompt-manager.js");
31
+ });
32
+
33
+ afterEach(() => {
34
+ rmSync(tempDir, { recursive: true, force: true });
35
+ });
36
+
37
+ describe("createPrompt", () => {
38
+ it("creates a global prompt", () => {
39
+ // Validates global prompts persist without project path coupling.
40
+ const prompt = promptManager.createPrompt("Review PR", "Review this PR and summarize risks", "global");
41
+ expect(prompt.scope).toBe("global");
42
+ expect(prompt.projectPath).toBeUndefined();
43
+ expect(prompt.projectPaths).toBeUndefined();
44
+ expect(prompt.id).toBeTruthy();
45
+ });
46
+
47
+ it("creates a project prompt with normalized path", () => {
48
+ // Validates project scope stores a normalized project root for later cwd matching.
49
+ const prompt = promptManager.createPrompt("Plan", "Plan this feature", "project", "/tmp/my-repo/");
50
+ expect(prompt.scope).toBe("project");
51
+ expect(prompt.projectPath).toBe("/tmp/my-repo");
52
+ expect(prompt.projectPaths).toEqual(["/tmp/my-repo"]);
53
+ });
54
+
55
+ it("creates a project prompt with multiple projectPaths", () => {
56
+ // Validates multi-folder targeting stores all paths normalized and deduplicated.
57
+ const prompt = promptManager.createPrompt(
58
+ "Multi",
59
+ "Multi-project prompt",
60
+ "project",
61
+ undefined,
62
+ ["/tmp/repo-a/", "/tmp/repo-b", "/tmp/repo-a/"],
63
+ );
64
+ expect(prompt.scope).toBe("project");
65
+ expect(prompt.projectPaths).toEqual(["/tmp/repo-a", "/tmp/repo-b"]);
66
+ // projectPath is set to the first path for backward compatibility
67
+ expect(prompt.projectPath).toBe("/tmp/repo-a");
68
+ });
69
+
70
+ it("merges projectPaths and legacy projectPath without duplicates", () => {
71
+ // When both projectPaths and projectPath are provided, they should be merged and deduped.
72
+ const prompt = promptManager.createPrompt(
73
+ "Merge",
74
+ "Merged",
75
+ "project",
76
+ "/tmp/repo-a",
77
+ ["/tmp/repo-b", "/tmp/repo-a"],
78
+ );
79
+ expect(prompt.projectPaths).toEqual(["/tmp/repo-b", "/tmp/repo-a"]);
80
+ });
81
+
82
+ it("rejects project prompts without a project path", () => {
83
+ expect(() => promptManager.createPrompt("Plan", "x", "project")).toThrow(
84
+ "Project path is required for project prompts",
85
+ );
86
+ });
87
+
88
+ it("rejects project prompts with empty projectPaths array", () => {
89
+ // An empty array is not valid for project scope.
90
+ expect(() => promptManager.createPrompt("Plan", "x", "project", undefined, [])).toThrow(
91
+ "Project path is required for project prompts",
92
+ );
93
+ });
94
+ });
95
+
96
+ describe("listPrompts", () => {
97
+ it("returns global + matching project prompts for cwd", () => {
98
+ // Verifies cwd filtering includes global prompts and only project prompts in the same repo subtree.
99
+ const global = promptManager.createPrompt("Global", "Global text", "global");
100
+ const project = promptManager.createPrompt("Project", "Project text", "project", "/tmp/repo");
101
+ promptManager.createPrompt("Other", "Other text", "project", "/tmp/other");
102
+
103
+ const prompts = promptManager.listPrompts({ cwd: "/tmp/repo/packages/ui" });
104
+ expect(prompts.map((p) => p.id)).toContain(global.id);
105
+ expect(prompts.map((p) => p.id)).toContain(project.id);
106
+ expect(prompts.map((p) => p.name)).not.toContain("Other");
107
+ });
108
+
109
+ it("matches cwd against any of the projectPaths", () => {
110
+ // A prompt with multiple projectPaths should be visible in any of those directories.
111
+ const multi = promptManager.createPrompt(
112
+ "Multi-target",
113
+ "Visible in both repos",
114
+ "project",
115
+ undefined,
116
+ ["/tmp/repo-a", "/tmp/repo-b"],
117
+ );
118
+
119
+ const inA = promptManager.listPrompts({ cwd: "/tmp/repo-a/src" });
120
+ expect(inA.map((p) => p.id)).toContain(multi.id);
121
+
122
+ const inB = promptManager.listPrompts({ cwd: "/tmp/repo-b" });
123
+ expect(inB.map((p) => p.id)).toContain(multi.id);
124
+
125
+ const inC = promptManager.listPrompts({ cwd: "/tmp/repo-c" });
126
+ expect(inC.map((p) => p.id)).not.toContain(multi.id);
127
+ });
128
+
129
+ it("reads legacy prompts with only projectPath (no projectPaths)", () => {
130
+ // Simulates loading a prompts.json that was created before projectPaths existed.
131
+ const companionDir = join(tempDir, ".companion");
132
+ mkdirSync(companionDir, { recursive: true });
133
+ writeFileSync(
134
+ join(companionDir, "prompts.json"),
135
+ JSON.stringify([
136
+ {
137
+ id: "legacy-1",
138
+ name: "Legacy",
139
+ content: "Legacy prompt",
140
+ scope: "project",
141
+ projectPath: "/tmp/legacy-repo",
142
+ createdAt: 1,
143
+ updatedAt: 2,
144
+ },
145
+ ]),
146
+ );
147
+
148
+ // Re-import to pick up the written file
149
+ const prompts = promptManager.listPrompts({ cwd: "/tmp/legacy-repo/src" });
150
+ expect(prompts.map((p) => p.id)).toContain("legacy-1");
151
+ });
152
+ });
153
+
154
+ describe("updatePrompt", () => {
155
+ it("updates a prompt name/content", () => {
156
+ // Ensures edits update mutable fields while preserving prompt identity.
157
+ const prompt = promptManager.createPrompt("Old", "Old content", "global");
158
+ const updated = promptManager.updatePrompt(prompt.id, { name: "New", content: "New content" });
159
+ expect(updated).not.toBeNull();
160
+ expect(updated!.name).toBe("New");
161
+ expect(updated!.content).toBe("New content");
162
+ });
163
+
164
+ it("changes scope from global to project with projectPaths", () => {
165
+ // Validates that update can change scope and set project paths.
166
+ const prompt = promptManager.createPrompt("Was global", "Some content", "global");
167
+ const updated = promptManager.updatePrompt(prompt.id, {
168
+ scope: "project",
169
+ projectPaths: ["/tmp/repo-x"],
170
+ });
171
+ expect(updated!.scope).toBe("project");
172
+ expect(updated!.projectPaths).toEqual(["/tmp/repo-x"]);
173
+ expect(updated!.projectPath).toBe("/tmp/repo-x");
174
+ });
175
+
176
+ it("changes scope from project to global and clears paths", () => {
177
+ // Validates that switching to global removes projectPaths.
178
+ const prompt = promptManager.createPrompt("Was project", "Content", "project", "/tmp/repo");
179
+ const updated = promptManager.updatePrompt(prompt.id, { scope: "global" });
180
+ expect(updated!.scope).toBe("global");
181
+ expect(updated!.projectPaths).toBeUndefined();
182
+ expect(updated!.projectPath).toBeUndefined();
183
+ });
184
+
185
+ it("rejects switching to project scope without paths", () => {
186
+ // Project scope without paths should fail validation.
187
+ const prompt = promptManager.createPrompt("Global", "Content", "global");
188
+ expect(() => promptManager.updatePrompt(prompt.id, { scope: "project" })).toThrow(
189
+ "Project path is required for project prompts",
190
+ );
191
+ });
192
+
193
+ it("updates projectPaths on an existing project prompt", () => {
194
+ // Validates that projectPaths can be updated independently.
195
+ const prompt = promptManager.createPrompt("Project", "Content", "project", "/tmp/repo-a");
196
+ const updated = promptManager.updatePrompt(prompt.id, {
197
+ projectPaths: ["/tmp/repo-b", "/tmp/repo-c"],
198
+ });
199
+ expect(updated!.projectPaths).toEqual(["/tmp/repo-b", "/tmp/repo-c"]);
200
+ expect(updated!.projectPath).toBe("/tmp/repo-b");
201
+ });
202
+ });
203
+
204
+ describe("deletePrompt", () => {
205
+ it("deletes a prompt", () => {
206
+ // Ensures a deleted prompt is no longer retrievable.
207
+ const prompt = promptManager.createPrompt("Delete me", "tmp", "global");
208
+ expect(promptManager.deletePrompt(prompt.id)).toBe(true);
209
+ expect(promptManager.getPrompt(prompt.id)).toBeNull();
210
+ });
211
+ });
@@ -0,0 +1,211 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { COMPANION_HOME } from "./paths.js";
4
+
5
+ export type PromptScope = "global" | "project";
6
+
7
+ export interface SavedPrompt {
8
+ id: string;
9
+ name: string;
10
+ content: string;
11
+ scope: PromptScope;
12
+ projectPath?: string;
13
+ projectPaths?: string[];
14
+ createdAt: number;
15
+ updatedAt: number;
16
+ }
17
+
18
+ export interface PromptUpdateFields {
19
+ name?: string;
20
+ content?: string;
21
+ scope?: PromptScope;
22
+ projectPaths?: string[];
23
+ }
24
+
25
+ const PROMPTS_FILE = join(COMPANION_HOME, "prompts.json");
26
+
27
+ function ensureDir(): void {
28
+ mkdirSync(COMPANION_HOME, { recursive: true });
29
+ }
30
+
31
+ function normalizePath(path: string): string {
32
+ return resolve(path).replace(/[\\/]+$/, "");
33
+ }
34
+
35
+ function loadPrompts(): SavedPrompt[] {
36
+ ensureDir();
37
+ if (!existsSync(PROMPTS_FILE)) return [];
38
+ try {
39
+ const raw = readFileSync(PROMPTS_FILE, "utf-8");
40
+ const parsed = JSON.parse(raw) as unknown;
41
+ if (!Array.isArray(parsed)) return [];
42
+ return parsed.filter((p): p is SavedPrompt => {
43
+ if (!p || typeof p !== "object") return false;
44
+ const candidate = p as Partial<SavedPrompt>;
45
+ return (
46
+ typeof candidate.id === "string"
47
+ && typeof candidate.name === "string"
48
+ && typeof candidate.content === "string"
49
+ && (candidate.scope === "global" || candidate.scope === "project")
50
+ );
51
+ });
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+
57
+ function savePrompts(prompts: SavedPrompt[]): void {
58
+ ensureDir();
59
+ writeFileSync(PROMPTS_FILE, JSON.stringify(prompts, null, 2), "utf-8");
60
+ }
61
+
62
+ function sortPrompts(prompts: SavedPrompt[]): SavedPrompt[] {
63
+ return [...prompts].sort((a, b) => b.updatedAt - a.updatedAt || a.name.localeCompare(b.name));
64
+ }
65
+
66
+ function visibleForCwd(prompt: SavedPrompt, cwd: string): boolean {
67
+ if (prompt.scope === "global") return true;
68
+ const paths = resolveProjectPaths(prompt);
69
+ if (paths.length === 0) return false;
70
+ const normalizedCwd = normalizePath(cwd);
71
+ return paths.some((p) => {
72
+ const normalizedProject = normalizePath(p);
73
+ return normalizedCwd === normalizedProject || normalizedCwd.startsWith(`${normalizedProject}/`);
74
+ });
75
+ }
76
+
77
+ /** Merges legacy projectPath and projectPaths into a single deduplicated list. */
78
+ function resolveProjectPaths(prompt: SavedPrompt): string[] {
79
+ const paths: string[] = [];
80
+ if (prompt.projectPaths && prompt.projectPaths.length > 0) {
81
+ paths.push(...prompt.projectPaths);
82
+ }
83
+ if (prompt.projectPath && !paths.some((p) => normalizePath(p) === normalizePath(prompt.projectPath!))) {
84
+ paths.push(prompt.projectPath);
85
+ }
86
+ return paths;
87
+ }
88
+
89
+ export function listPrompts(opts?: { cwd?: string; scope?: "global" | "project" | "all" }): SavedPrompt[] {
90
+ const prompts = loadPrompts();
91
+ const scope = opts?.scope ?? "all";
92
+
93
+ const filteredByScope = prompts.filter((p) => {
94
+ if (scope === "all") return true;
95
+ return p.scope === scope;
96
+ });
97
+
98
+ if (!opts?.cwd) return sortPrompts(filteredByScope);
99
+
100
+ return sortPrompts(filteredByScope.filter((p) => visibleForCwd(p, opts.cwd!)));
101
+ }
102
+
103
+ export function getPrompt(id: string): SavedPrompt | null {
104
+ return loadPrompts().find((p) => p.id === id) ?? null;
105
+ }
106
+
107
+ export function createPrompt(
108
+ name: string,
109
+ content: string,
110
+ scope: PromptScope,
111
+ projectPath?: string,
112
+ projectPaths?: string[],
113
+ ): SavedPrompt {
114
+ const cleanName = name?.trim();
115
+ const cleanContent = content?.trim();
116
+ if (!cleanName) throw new Error("Prompt name is required");
117
+ if (!cleanContent) throw new Error("Prompt content is required");
118
+ if (scope !== "global" && scope !== "project") throw new Error("Invalid prompt scope");
119
+
120
+ // Merge projectPaths and legacy projectPath into a single deduplicated list
121
+ const mergedPaths = dedupeAndNormalizePaths(projectPaths, projectPath);
122
+ if (scope === "project" && mergedPaths.length === 0) {
123
+ throw new Error("Project path is required for project prompts");
124
+ }
125
+
126
+ const prompts = loadPrompts();
127
+ const now = Date.now();
128
+ const prompt: SavedPrompt = {
129
+ id: crypto.randomUUID(),
130
+ name: cleanName,
131
+ content: cleanContent,
132
+ scope,
133
+ projectPath: scope === "project" ? mergedPaths[0] : undefined,
134
+ projectPaths: scope === "project" ? mergedPaths : undefined,
135
+ createdAt: now,
136
+ updatedAt: now,
137
+ };
138
+ prompts.push(prompt);
139
+ savePrompts(prompts);
140
+ return prompt;
141
+ }
142
+
143
+ function dedupeAndNormalizePaths(paths?: string[], legacyPath?: string): string[] {
144
+ const seen = new Set<string>();
145
+ const result: string[] = [];
146
+ const all = [...(paths ?? []), ...(legacyPath?.trim() ? [legacyPath] : [])];
147
+ for (const p of all) {
148
+ const trimmed = p.trim();
149
+ if (!trimmed) continue;
150
+ const normalized = normalizePath(trimmed);
151
+ if (!seen.has(normalized)) {
152
+ seen.add(normalized);
153
+ result.push(normalized);
154
+ }
155
+ }
156
+ return result;
157
+ }
158
+
159
+ export function updatePrompt(id: string, updates: PromptUpdateFields): SavedPrompt | null {
160
+ const prompts = loadPrompts();
161
+ const index = prompts.findIndex((p) => p.id === id);
162
+ if (index < 0) return null;
163
+
164
+ if (updates.name !== undefined && !updates.name.trim()) {
165
+ throw new Error("Prompt name cannot be empty");
166
+ }
167
+ if (updates.content !== undefined && !updates.content.trim()) {
168
+ throw new Error("Prompt content cannot be empty");
169
+ }
170
+
171
+ const newScope = updates.scope ?? prompts[index].scope;
172
+ if (updates.scope !== undefined && updates.scope !== "global" && updates.scope !== "project") {
173
+ throw new Error("Invalid prompt scope");
174
+ }
175
+
176
+ let newProjectPaths = prompts[index].projectPaths;
177
+ let newProjectPath = prompts[index].projectPath;
178
+ if (updates.projectPaths !== undefined) {
179
+ const normalized = dedupeAndNormalizePaths(updates.projectPaths);
180
+ newProjectPaths = normalized.length > 0 ? normalized : undefined;
181
+ newProjectPath = normalized.length > 0 ? normalized[0] : undefined;
182
+ }
183
+ if (newScope === "project" && (!newProjectPaths || newProjectPaths.length === 0)) {
184
+ throw new Error("Project path is required for project prompts");
185
+ }
186
+ if (newScope === "global") {
187
+ newProjectPaths = undefined;
188
+ newProjectPath = undefined;
189
+ }
190
+
191
+ const updated: SavedPrompt = {
192
+ ...prompts[index],
193
+ name: updates.name !== undefined ? updates.name.trim() : prompts[index].name,
194
+ content: updates.content !== undefined ? updates.content.trim() : prompts[index].content,
195
+ scope: newScope,
196
+ projectPath: newProjectPath,
197
+ projectPaths: newProjectPaths,
198
+ updatedAt: Date.now(),
199
+ };
200
+ prompts[index] = updated;
201
+ savePrompts(prompts);
202
+ return updated;
203
+ }
204
+
205
+ export function deletePrompt(id: string): boolean {
206
+ const prompts = loadPrompts();
207
+ const next = prompts.filter((p) => p.id !== id);
208
+ if (next.length === prompts.length) return false;
209
+ savePrompts(next);
210
+ return true;
211
+ }
@@ -0,0 +1,19 @@
1
+ # Claude Upstream Snapshot
2
+
3
+ This folder contains an offline snapshot of the official Claude Agent SDK TypeScript
4
+ surface used by the bridge compatibility tests.
5
+
6
+ Source package:
7
+ - `@anthropic-ai/claude-agent-sdk@0.2.41`
8
+ - tarball: `https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.41.tgz`
9
+
10
+ Files:
11
+ - `sdk.d.ts.txt` — copied from `package/sdk.d.ts` in the npm tarball
12
+
13
+ Refresh command (example):
14
+ ```bash
15
+ TARBALL=$(npm view @anthropic-ai/claude-agent-sdk dist.tarball)
16
+ curl -fsSL "$TARBALL" -o /tmp/claude-agent-sdk.tgz
17
+ tar -xzf /tmp/claude-agent-sdk.tgz -C /tmp package/sdk.d.ts
18
+ cp /tmp/package/sdk.d.ts web/server/protocol/claude-upstream/sdk.d.ts.txt
19
+ ```