@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1

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 (246) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
  3. package/dist/cli.js +10009 -9654
  4. package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
  5. package/dist/types/advisor/runtime.d.ts +8 -2
  6. package/dist/types/advisor/transcript-recorder.d.ts +15 -0
  7. package/dist/types/auto-thinking/classifier.d.ts +9 -2
  8. package/dist/types/cli/args.d.ts +2 -0
  9. package/dist/types/cli/session-picker.d.ts +14 -7
  10. package/dist/types/collab/guest.d.ts +21 -2
  11. package/dist/types/commands/launch.d.ts +6 -0
  12. package/dist/types/config/model-discovery.d.ts +12 -0
  13. package/dist/types/config/settings-schema.d.ts +73 -8
  14. package/dist/types/cursor-bridge-tools.d.ts +54 -0
  15. package/dist/types/cursor.d.ts +193 -8
  16. package/dist/types/edit/edit-clipboard.d.ts +19 -0
  17. package/dist/types/edit/hashline/diff.d.ts +9 -1
  18. package/dist/types/edit/index.d.ts +8 -1
  19. package/dist/types/edit/streaming.d.ts +8 -1
  20. package/dist/types/extensibility/extensions/runner.d.ts +19 -2
  21. package/dist/types/extensibility/extensions/types.d.ts +25 -1
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
  23. package/dist/types/internal-urls/index.d.ts +1 -0
  24. package/dist/types/internal-urls/local-protocol.d.ts +3 -2
  25. package/dist/types/internal-urls/security-protocol.d.ts +15 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
  28. package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
  29. package/dist/types/launch/terminal-output-worker.d.ts +1 -0
  30. package/dist/types/lsp/client.d.ts +2 -0
  31. package/dist/types/main.d.ts +2 -0
  32. package/dist/types/mcp/manager.d.ts +33 -4
  33. package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +12 -0
  35. package/dist/types/modes/components/session-selector.d.ts +6 -0
  36. package/dist/types/modes/components/status-line/component.d.ts +23 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +16 -7
  38. package/dist/types/modes/components/usage-row.d.ts +2 -0
  39. package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
  40. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  41. package/dist/types/modes/interactive-mode.d.ts +4 -2
  42. package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
  43. package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
  44. package/dist/types/modes/theme/theme.d.ts +12 -1
  45. package/dist/types/modes/types.d.ts +2 -1
  46. package/dist/types/sdk.d.ts +18 -0
  47. package/dist/types/security/auth.d.ts +30 -0
  48. package/dist/types/security/cloud.d.ts +79 -0
  49. package/dist/types/security/comparison.d.ts +49 -0
  50. package/dist/types/security/contracts/ids.d.ts +15 -0
  51. package/dist/types/security/contracts/index.d.ts +4 -0
  52. package/dist/types/security/contracts/schemas.d.ts +660 -0
  53. package/dist/types/security/contracts/types.d.ts +234 -0
  54. package/dist/types/security/contracts/validation.d.ts +5 -0
  55. package/dist/types/security/coordinator.d.ts +100 -0
  56. package/dist/types/security/importers/codex-security.d.ts +7 -0
  57. package/dist/types/security/importers/index.d.ts +2 -0
  58. package/dist/types/security/importers/sarif.d.ts +9 -0
  59. package/dist/types/security/index.d.ts +13 -0
  60. package/dist/types/security/preflight.d.ts +61 -0
  61. package/dist/types/security/provenance.d.ts +24 -0
  62. package/dist/types/security/publication.d.ts +78 -0
  63. package/dist/types/security/remediation.d.ts +27 -0
  64. package/dist/types/security/resource-output.d.ts +8 -0
  65. package/dist/types/security/sarif.d.ts +2 -0
  66. package/dist/types/security/store.d.ts +40 -0
  67. package/dist/types/session/agent-session-events.d.ts +2 -0
  68. package/dist/types/session/agent-session-types.d.ts +36 -1
  69. package/dist/types/session/agent-session.d.ts +4 -2
  70. package/dist/types/session/claude-session-store.d.ts +13 -0
  71. package/dist/types/session/codex-session-store.d.ts +14 -0
  72. package/dist/types/session/credential-pin.d.ts +58 -0
  73. package/dist/types/session/foreign-session-import.d.ts +15 -0
  74. package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
  75. package/dist/types/session/foreign-session-store.d.ts +23 -0
  76. package/dist/types/session/session-advisors.d.ts +47 -2
  77. package/dist/types/session/session-entries.d.ts +19 -1
  78. package/dist/types/session/session-handoff.d.ts +4 -1
  79. package/dist/types/session/session-loader.d.ts +2 -0
  80. package/dist/types/session/session-maintenance.d.ts +1 -1
  81. package/dist/types/session/session-manager.d.ts +21 -0
  82. package/dist/types/session/session-tools.d.ts +25 -1
  83. package/dist/types/session/turn-recovery.d.ts +10 -4
  84. package/dist/types/slash-commands/helpers/security.d.ts +2 -0
  85. package/dist/types/system-prompt.d.ts +4 -1
  86. package/dist/types/task/executor.d.ts +14 -2
  87. package/dist/types/task/render.d.ts +7 -0
  88. package/dist/types/thinking.d.ts +18 -6
  89. package/dist/types/tools/acp-bridge.d.ts +28 -5
  90. package/dist/types/tools/builtin-names.d.ts +1 -1
  91. package/dist/types/tools/grep.d.ts +17 -1
  92. package/dist/types/tools/hub/launch.d.ts +5 -1
  93. package/dist/types/tools/index.d.ts +10 -2
  94. package/dist/types/tools/path-utils.d.ts +19 -0
  95. package/dist/types/tools/read.d.ts +2 -0
  96. package/dist/types/tools/security-scan.d.ts +96 -0
  97. package/dist/types/tools/xdev.d.ts +2 -1
  98. package/dist/types/utils/changelog.d.ts +19 -0
  99. package/dist/types/utils/git.d.ts +10 -0
  100. package/dist/types/utils/jj.d.ts +11 -4
  101. package/dist/types/web/search/types.d.ts +2 -2
  102. package/package.json +12 -12
  103. package/scripts/security-compare.ts +40 -0
  104. package/src/advisor/runtime.ts +138 -12
  105. package/src/advisor/transcript-recorder.ts +56 -0
  106. package/src/auto-thinking/classifier.ts +48 -10
  107. package/src/cli/args.ts +6 -0
  108. package/src/cli/flag-tables.ts +2 -0
  109. package/src/cli/gallery-fixtures/fs.ts +25 -0
  110. package/src/cli/session-picker.ts +34 -17
  111. package/src/cli/ttsr-cli.ts +19 -1
  112. package/src/cli.ts +9 -0
  113. package/src/collab/guest.ts +37 -4
  114. package/src/collab/host.ts +1 -0
  115. package/src/commands/launch.ts +6 -0
  116. package/src/config/model-discovery.ts +32 -5
  117. package/src/config/model-registry.ts +107 -25
  118. package/src/config/settings-schema.ts +77 -6
  119. package/src/config/settings.ts +25 -0
  120. package/src/cursor-bridge-tools.ts +81 -0
  121. package/src/cursor.ts +463 -12
  122. package/src/edit/edit-clipboard.ts +23 -0
  123. package/src/edit/hashline/diff.ts +49 -10
  124. package/src/edit/hashline/execute.ts +38 -6
  125. package/src/edit/hashline/filesystem.ts +27 -3
  126. package/src/edit/index.ts +12 -2
  127. package/src/edit/renderer.ts +4 -4
  128. package/src/edit/streaming.ts +17 -2
  129. package/src/eval/py/prelude.py +7 -3
  130. package/src/export/html/template.js +1 -1
  131. package/src/extensibility/extensions/runner.ts +70 -0
  132. package/src/extensibility/extensions/types.ts +30 -0
  133. package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
  134. package/src/internal-urls/index.ts +1 -0
  135. package/src/internal-urls/local-protocol.ts +3 -2
  136. package/src/internal-urls/mcp-protocol.ts +4 -1
  137. package/src/internal-urls/router.ts +4 -1
  138. package/src/internal-urls/security-protocol.ts +261 -0
  139. package/src/internal-urls/types.ts +1 -1
  140. package/src/launch/terminal-output-worker-client.ts +53 -0
  141. package/src/launch/terminal-output-worker-protocol.ts +11 -0
  142. package/src/launch/terminal-output-worker.ts +23 -0
  143. package/src/lsp/client.ts +18 -3
  144. package/src/lsp/index.ts +7 -0
  145. package/src/lsp/render.ts +1 -1
  146. package/src/main.ts +127 -56
  147. package/src/mcp/manager.ts +120 -18
  148. package/src/modes/acp/acp-agent.ts +15 -9
  149. package/src/modes/components/chat-transcript-builder.ts +30 -11
  150. package/src/modes/components/codex-reset-fireworks.ts +369 -0
  151. package/src/modes/components/read-tool-group.ts +159 -17
  152. package/src/modes/components/session-selector.ts +19 -3
  153. package/src/modes/components/status-line/component.ts +381 -54
  154. package/src/modes/components/tool-execution.ts +78 -31
  155. package/src/modes/components/usage-row.ts +10 -5
  156. package/src/modes/controllers/event-controller.ts +26 -4
  157. package/src/modes/controllers/mcp-command-controller.ts +68 -3
  158. package/src/modes/controllers/selector-controller.ts +94 -36
  159. package/src/modes/controllers/tan-command-controller.ts +14 -0
  160. package/src/modes/interactive-mode.ts +31 -17
  161. package/src/modes/rpc/host-uris.ts +6 -0
  162. package/src/modes/rpc/rpc-client.ts +18 -0
  163. package/src/modes/rpc/rpc-mode.ts +16 -1
  164. package/src/modes/rpc/rpc-types.ts +11 -0
  165. package/src/modes/theme/theme.ts +151 -6
  166. package/src/modes/types.ts +2 -1
  167. package/src/modes/utils/ui-helpers.ts +38 -21
  168. package/src/prompts/agents/security-reviewer.md +75 -0
  169. package/src/prompts/security/scan-coordinator.md +7 -0
  170. package/src/prompts/security/scan-request.md +21 -0
  171. package/src/prompts/security/validate-request.md +8 -0
  172. package/src/prompts/system/auto-thinking-difficulty.md +4 -2
  173. package/src/prompts/system/plan-mode-active.md +2 -2
  174. package/src/prompts/system/system-prompt.md +6 -0
  175. package/src/prompts/system/xdev-mount-notice.md +1 -1
  176. package/src/prompts/tools/bash.md +15 -17
  177. package/src/prompts/tools/checkpoint.md +1 -1
  178. package/src/prompts/tools/glob.md +7 -6
  179. package/src/prompts/tools/grep.md +7 -6
  180. package/src/prompts/tools/security-publish.md +1 -0
  181. package/src/prompts/tools/security-scan.md +1 -0
  182. package/src/sdk.ts +212 -21
  183. package/src/security/auth.ts +98 -0
  184. package/src/security/cloud.ts +686 -0
  185. package/src/security/comparison.ts +255 -0
  186. package/src/security/contracts/ids.ts +111 -0
  187. package/src/security/contracts/index.ts +4 -0
  188. package/src/security/contracts/schemas.ts +201 -0
  189. package/src/security/contracts/types.ts +254 -0
  190. package/src/security/contracts/validation.ts +65 -0
  191. package/src/security/coordinator.ts +708 -0
  192. package/src/security/importers/codex-security.ts +387 -0
  193. package/src/security/importers/index.ts +2 -0
  194. package/src/security/importers/sarif.ts +357 -0
  195. package/src/security/index.ts +13 -0
  196. package/src/security/preflight.ts +405 -0
  197. package/src/security/provenance.ts +106 -0
  198. package/src/security/publication.ts +326 -0
  199. package/src/security/remediation.ts +93 -0
  200. package/src/security/resource-output.ts +50 -0
  201. package/src/security/sarif.ts +78 -0
  202. package/src/security/store.ts +430 -0
  203. package/src/session/agent-session-events.ts +1 -0
  204. package/src/session/agent-session-types.ts +40 -1
  205. package/src/session/agent-session.ts +302 -175
  206. package/src/session/claude-session-store.ts +426 -0
  207. package/src/session/codex-session-store.ts +673 -0
  208. package/src/session/credential-pin.ts +93 -0
  209. package/src/session/foreign-session-import.ts +52 -0
  210. package/src/session/foreign-session-jsonl.ts +29 -0
  211. package/src/session/foreign-session-store.ts +26 -0
  212. package/src/session/model-controls.ts +11 -3
  213. package/src/session/session-advisors.ts +140 -16
  214. package/src/session/session-entries.ts +21 -1
  215. package/src/session/session-handoff.ts +20 -3
  216. package/src/session/session-loader.ts +25 -9
  217. package/src/session/session-maintenance.ts +59 -11
  218. package/src/session/session-manager.ts +61 -0
  219. package/src/session/session-tools.ts +107 -5
  220. package/src/session/settings-stream-fn.ts +1 -0
  221. package/src/session/turn-recovery.ts +97 -51
  222. package/src/slash-commands/builtin-registry.ts +152 -3
  223. package/src/slash-commands/helpers/security.ts +451 -0
  224. package/src/system-prompt.ts +7 -2
  225. package/src/task/agents.ts +2 -0
  226. package/src/task/executor.ts +20 -4
  227. package/src/task/render.ts +20 -4
  228. package/src/task/structured-subagent.ts +6 -4
  229. package/src/thinking.ts +29 -10
  230. package/src/tools/acp-bridge.ts +52 -8
  231. package/src/tools/browser.ts +5 -0
  232. package/src/tools/builtin-names.ts +1 -0
  233. package/src/tools/checkpoint.ts +0 -13
  234. package/src/tools/grep.ts +64 -8
  235. package/src/tools/hub/launch.ts +10 -15
  236. package/src/tools/index.ts +37 -5
  237. package/src/tools/path-utils.ts +91 -0
  238. package/src/tools/read.ts +3 -0
  239. package/src/tools/security-scan.ts +287 -0
  240. package/src/tools/write.ts +9 -3
  241. package/src/tools/xdev.ts +39 -14
  242. package/src/utils/changelog.ts +144 -4
  243. package/src/utils/git.ts +61 -14
  244. package/src/utils/jj.ts +19 -13
  245. package/src/web/search/providers/codex.ts +34 -0
  246. package/src/web/search/types.ts +1 -1
@@ -0,0 +1,430 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { getSecurityProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
4
+ import { withFileLock } from "../config/file-lock";
5
+ import * as git from "../utils/git";
6
+ import { compareSecurityLineage } from "./comparison";
7
+ import type {
8
+ SecurityComparisonReport,
9
+ SecurityDisposition,
10
+ SecurityEvidence,
11
+ SecurityFinding,
12
+ SecurityScan,
13
+ SecurityScanBundle,
14
+ SecurityScanPlan,
15
+ SecurityValidation,
16
+ } from "./contracts";
17
+ import {
18
+ encodeSecurityProjectKey,
19
+ parseSecurityFinding,
20
+ parseSecurityScan,
21
+ parseSecurityScanBundle,
22
+ parseSecurityScanPlan,
23
+ } from "./contracts";
24
+ import { createPublicSecurityScan, redactPrivateSecurityMetadata } from "./provenance";
25
+ import { exportSecurityBundleToSarif } from "./sarif";
26
+
27
+ const STORE_SCHEMA_VERSION = 1;
28
+ const PRIVATE_DIRECTORY_MODE = 0o700;
29
+ const PRIVATE_FILE_MODE = 0o600;
30
+
31
+ /** Serialize project-store read/modify/write transactions within this process. */
32
+ const SECURITY_STORE_WRITE_CHAINS = new Map<string, Promise<unknown>>();
33
+
34
+ async function withSecurityStoreWrite<T>(key: string, operation: () => Promise<T>): Promise<T> {
35
+ const lockTarget = path.join(key, "index.json");
36
+ const run = (SECURITY_STORE_WRITE_CHAINS.get(key) ?? Promise.resolve()).then(() =>
37
+ withFileLock(lockTarget, operation, { staleMs: 60_000, retries: 200, retryDelayMs: 50 }),
38
+ );
39
+ const guarded = run.catch(() => undefined);
40
+ SECURITY_STORE_WRITE_CHAINS.set(key, guarded);
41
+ try {
42
+ return await run;
43
+ } finally {
44
+ if (SECURITY_STORE_WRITE_CHAINS.get(key) === guarded) SECURITY_STORE_WRITE_CHAINS.delete(key);
45
+ }
46
+ }
47
+
48
+ interface SecurityStoreIndex {
49
+ schemaVersion: 1;
50
+ projectKey: string;
51
+ repositoryRoot: string;
52
+ scanIds: string[];
53
+ planIds: string[];
54
+ updatedAt: string;
55
+ }
56
+
57
+ export interface SecurityScanSummary {
58
+ id: string;
59
+ status: SecurityScan["status"];
60
+ createdAt: string;
61
+ completedAt?: string;
62
+ producer: SecurityScan["producer"];
63
+ findingCount: number;
64
+ target: SecurityScan["target"];
65
+ }
66
+
67
+ export interface SecurityStoreOptions {
68
+ stateRoot?: string;
69
+ signal?: AbortSignal;
70
+ }
71
+
72
+ async function ensurePrivateDirectory(directory: string): Promise<void> {
73
+ await fs.mkdir(directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
74
+ if (process.platform !== "win32") await fs.chmod(directory, PRIVATE_DIRECTORY_MODE);
75
+ }
76
+
77
+ export interface SecurityFileWriteOptions {
78
+ hardenParent?: boolean;
79
+ }
80
+
81
+ export async function writeSecurityFileAtomic(
82
+ filePath: string,
83
+ content: string,
84
+ options: SecurityFileWriteOptions = {},
85
+ ): Promise<void> {
86
+ if (options.hardenParent ?? true) {
87
+ await ensurePrivateDirectory(path.dirname(filePath));
88
+ } else {
89
+ await fs.mkdir(path.dirname(filePath), { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
90
+ }
91
+ const temporaryPath = `${filePath}.${process.pid}.${Bun.randomUUIDv7()}.tmp`;
92
+ try {
93
+ // Bun.write cannot create exclusively; `wx` keeps concurrent atomic writers from sharing a temp file.
94
+ await fs.writeFile(temporaryPath, content, { encoding: "utf-8", mode: PRIVATE_FILE_MODE, flag: "wx" });
95
+ if (process.platform !== "win32") await fs.chmod(temporaryPath, PRIVATE_FILE_MODE);
96
+ try {
97
+ await fs.rename(temporaryPath, filePath);
98
+ } catch (error) {
99
+ const code = error instanceof Error && "code" in error ? String(error.code) : "";
100
+ if (process.platform !== "win32" || (code !== "EEXIST" && code !== "EPERM")) throw error;
101
+ await fs.rm(filePath, { force: true });
102
+ await fs.rename(temporaryPath, filePath);
103
+ }
104
+ } finally {
105
+ await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
106
+ }
107
+ }
108
+ export async function writeSecurityBundleToDirectory(directory: string, input: SecurityScanBundle): Promise<void> {
109
+ const bundle = parseSecurityScanBundle(input);
110
+ const root = path.resolve(directory);
111
+ await ensurePrivateDirectory(root);
112
+ await writeSecurityFileAtomic(path.join(root, "findings.json"), `${JSON.stringify(bundle.findings, null, 2)}\n`);
113
+ if (bundle.report !== undefined) {
114
+ await writeSecurityFileAtomic(path.join(root, "report.md"), bundle.report);
115
+ } else {
116
+ await fs.rm(path.join(root, "report.md"), { force: true });
117
+ }
118
+ if (bundle.sarif !== undefined) {
119
+ await writeSecurityFileAtomic(path.join(root, "results.sarif"), `${JSON.stringify(bundle.sarif, null, 2)}\n`);
120
+ } else {
121
+ await fs.rm(path.join(root, "results.sarif"), { force: true });
122
+ }
123
+ await writeSecurityFileAtomic(
124
+ path.join(root, "provenance.json"),
125
+ `${JSON.stringify(redactPrivateSecurityMetadata(bundle.scan.provenance), null, 2)}\n`,
126
+ );
127
+ // The scan manifest is the commit marker for directory consumers.
128
+ await writeSecurityFileAtomic(
129
+ path.join(root, "scan.json"),
130
+ `${JSON.stringify(createPublicSecurityScan(bundle.scan), null, 2)}\n`,
131
+ );
132
+ }
133
+
134
+ async function readJsonFile(filePath: string): Promise<unknown> {
135
+ return JSON.parse(await Bun.file(filePath).text()) as unknown;
136
+ }
137
+
138
+ async function readOptionalText(filePath: string): Promise<string | undefined> {
139
+ try {
140
+ return await Bun.file(filePath).text();
141
+ } catch (error) {
142
+ if (isEnoent(error)) return undefined;
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ export class SecurityStore {
148
+ readonly #repositoryRoot: string;
149
+ readonly #projectKey: string;
150
+ readonly #projectDirectory: string;
151
+
152
+ constructor(repositoryRoot: string, projectKey: string, projectDirectory: string) {
153
+ this.#repositoryRoot = repositoryRoot;
154
+ this.#projectKey = projectKey;
155
+ this.#projectDirectory = projectDirectory;
156
+ }
157
+
158
+ static async open(repositoryRoot: string, options: SecurityStoreOptions = {}): Promise<SecurityStore> {
159
+ const canonicalRoot = await fs.realpath(path.resolve(repositoryRoot)).catch(() => path.resolve(repositoryRoot));
160
+ const projectKey = encodeSecurityProjectKey(canonicalRoot);
161
+ const projectDirectory = options.stateRoot
162
+ ? path.join(path.resolve(options.stateRoot), projectKey)
163
+ : getSecurityProjectDir(projectKey);
164
+ await ensurePrivateDirectory(projectDirectory);
165
+ const store = new SecurityStore(canonicalRoot, projectKey, projectDirectory);
166
+ await withSecurityStoreWrite(projectDirectory, () => store.#ensureIndex());
167
+ return store;
168
+ }
169
+
170
+ static async openForCwd(cwd: string, options: SecurityStoreOptions = {}): Promise<SecurityStore> {
171
+ const resolvedCwd = path.resolve(cwd);
172
+ const repositoryRoot = (await git.repo.root(resolvedCwd, options.signal)) ?? resolvedCwd;
173
+ return SecurityStore.open(repositoryRoot, options);
174
+ }
175
+
176
+ get repositoryRoot(): string {
177
+ return this.#repositoryRoot;
178
+ }
179
+
180
+ get projectKey(): string {
181
+ return this.#projectKey;
182
+ }
183
+
184
+ get projectDirectory(): string {
185
+ return this.#projectDirectory;
186
+ }
187
+
188
+ #scanDirectory(scanId: string): string {
189
+ if (!/^secscan_[a-zA-Z0-9]+$/.test(scanId)) throw new Error(`Invalid security scan id: ${scanId}`);
190
+ return path.join(this.#projectDirectory, "scans", scanId);
191
+ }
192
+
193
+ #planPath(planId: string): string {
194
+ if (!/^secplan_[a-zA-Z0-9]+$/.test(planId)) throw new Error(`Invalid security plan id: ${planId}`);
195
+ return path.join(this.#projectDirectory, "plans", `${planId}.json`);
196
+ }
197
+
198
+ #indexPath(): string {
199
+ return path.join(this.#projectDirectory, "index.json");
200
+ }
201
+
202
+ async #ensureIndex(): Promise<void> {
203
+ try {
204
+ await this.#readIndex();
205
+ } catch (error) {
206
+ if (!isEnoent(error)) throw error;
207
+ await this.#writeIndex({
208
+ schemaVersion: STORE_SCHEMA_VERSION,
209
+ projectKey: this.#projectKey,
210
+ repositoryRoot: this.#repositoryRoot,
211
+ scanIds: [],
212
+ planIds: [],
213
+ updatedAt: new Date().toISOString(),
214
+ });
215
+ }
216
+ }
217
+
218
+ async #readIndex(): Promise<SecurityStoreIndex> {
219
+ const value = (await readJsonFile(this.#indexPath())) as Partial<SecurityStoreIndex>;
220
+ if (value.schemaVersion !== STORE_SCHEMA_VERSION || value.projectKey !== this.#projectKey) {
221
+ throw new Error(`Unsupported security store index at ${this.#indexPath()}`);
222
+ }
223
+ if (!Array.isArray(value.scanIds) || !value.scanIds.every(id => typeof id === "string")) {
224
+ throw new Error(`Invalid security store scan index at ${this.#indexPath()}`);
225
+ }
226
+ if (
227
+ value.planIds !== undefined &&
228
+ (!Array.isArray(value.planIds) || !value.planIds.every(id => typeof id === "string"))
229
+ ) {
230
+ throw new Error(`Invalid security store plan index at ${this.#indexPath()}`);
231
+ }
232
+ return { ...value, planIds: value.planIds ?? [] } as SecurityStoreIndex;
233
+ }
234
+
235
+ async #writeIndex(index: SecurityStoreIndex): Promise<void> {
236
+ await writeSecurityFileAtomic(this.#indexPath(), `${JSON.stringify(index, null, 2)}\n`);
237
+ }
238
+
239
+ async #putBundleUnlocked(input: SecurityScanBundle): Promise<void> {
240
+ const bundle = parseSecurityScanBundle(input);
241
+ if (bundle.scan.projectKey !== this.#projectKey) {
242
+ throw new Error(`Security scan project key ${bundle.scan.projectKey} does not match ${this.#projectKey}`);
243
+ }
244
+ const scanDirectory = this.#scanDirectory(bundle.scan.id);
245
+ await ensurePrivateDirectory(scanDirectory);
246
+ await writeSecurityFileAtomic(
247
+ path.join(scanDirectory, "findings.json"),
248
+ `${JSON.stringify(bundle.findings, null, 2)}\n`,
249
+ );
250
+ if (bundle.report !== undefined) {
251
+ await writeSecurityFileAtomic(path.join(scanDirectory, "report.md"), bundle.report);
252
+ } else {
253
+ await fs.rm(path.join(scanDirectory, "report.md"), { force: true });
254
+ }
255
+ if (bundle.sarif !== undefined) {
256
+ await writeSecurityFileAtomic(
257
+ path.join(scanDirectory, "results.sarif"),
258
+ `${JSON.stringify(bundle.sarif, null, 2)}\n`,
259
+ );
260
+ } else {
261
+ await fs.rm(path.join(scanDirectory, "results.sarif"), { force: true });
262
+ }
263
+ // The scan manifest is the commit marker: readers never observe it before
264
+ // its findings and optional artifacts have been written atomically.
265
+ await writeSecurityFileAtomic(path.join(scanDirectory, "scan.json"), `${JSON.stringify(bundle.scan, null, 2)}\n`);
266
+ const index = await this.#readIndex();
267
+ if (!index.scanIds.includes(bundle.scan.id)) index.scanIds.push(bundle.scan.id);
268
+ index.updatedAt = new Date().toISOString();
269
+ await this.#writeIndex(index);
270
+ }
271
+
272
+ async putBundle(input: SecurityScanBundle): Promise<void> {
273
+ await withSecurityStoreWrite(this.#projectDirectory, () => this.#putBundleUnlocked(input));
274
+ }
275
+
276
+ async putPlan(input: SecurityScanPlan): Promise<void> {
277
+ await withSecurityStoreWrite(this.#projectDirectory, async () => {
278
+ const plan = parseSecurityScanPlan(input);
279
+ if (plan.repositoryRoot !== this.#repositoryRoot) {
280
+ throw new Error(`Security plan repository ${plan.repositoryRoot} does not match ${this.#repositoryRoot}`);
281
+ }
282
+ await writeSecurityFileAtomic(this.#planPath(plan.id), `${JSON.stringify(plan, null, 2)}\n`);
283
+ const index = await this.#readIndex();
284
+ if (!index.planIds.includes(plan.id)) index.planIds.push(plan.id);
285
+ index.updatedAt = new Date().toISOString();
286
+ await this.#writeIndex(index);
287
+ });
288
+ }
289
+
290
+ async getPlan(planId: string): Promise<SecurityScanPlan | null> {
291
+ try {
292
+ return parseSecurityScanPlan(await readJsonFile(this.#planPath(planId)));
293
+ } catch (error) {
294
+ if (isEnoent(error)) return null;
295
+ throw error;
296
+ }
297
+ }
298
+
299
+ async listPlans(): Promise<SecurityScanPlan[]> {
300
+ const index = await this.#readIndex();
301
+ const plans: SecurityScanPlan[] = [];
302
+ for (const planId of [...index.planIds].reverse()) {
303
+ const plan = await this.getPlan(planId);
304
+ if (plan) plans.push(plan);
305
+ }
306
+ return plans;
307
+ }
308
+
309
+ async getScan(scanId: string): Promise<SecurityScan | null> {
310
+ try {
311
+ return parseSecurityScan(await readJsonFile(path.join(this.#scanDirectory(scanId), "scan.json")));
312
+ } catch (error) {
313
+ if (isEnoent(error)) return null;
314
+ throw error;
315
+ }
316
+ }
317
+
318
+ async #getBundleUnlocked(scanId: string): Promise<SecurityScanBundle | null> {
319
+ const scan = await this.getScan(scanId);
320
+ if (!scan) return null;
321
+ const rawFindings = await readJsonFile(path.join(this.#scanDirectory(scanId), "findings.json"));
322
+ if (!Array.isArray(rawFindings)) throw new Error(`Invalid findings list for ${scanId}`);
323
+ const findings = rawFindings.map(parseSecurityFinding);
324
+ const report = await readOptionalText(path.join(this.#scanDirectory(scanId), "report.md"));
325
+ const sarifText = await readOptionalText(path.join(this.#scanDirectory(scanId), "results.sarif"));
326
+ const bundle: SecurityScanBundle = { scan, findings };
327
+ if (report !== undefined) bundle.report = report;
328
+ if (sarifText !== undefined) bundle.sarif = JSON.parse(sarifText) as Record<string, unknown>;
329
+ return parseSecurityScanBundle(bundle);
330
+ }
331
+
332
+ async getBundle(scanId: string): Promise<SecurityScanBundle | null> {
333
+ return withSecurityStoreWrite(this.#projectDirectory, () => this.#getBundleUnlocked(scanId));
334
+ }
335
+
336
+ async listScans(): Promise<SecurityScanSummary[]> {
337
+ const index = await this.#readIndex();
338
+ const summaries: SecurityScanSummary[] = [];
339
+ for (const scanId of [...index.scanIds].reverse()) {
340
+ const bundle = await this.getBundle(scanId);
341
+ if (!bundle) continue;
342
+ summaries.push({
343
+ id: bundle.scan.id,
344
+ status: bundle.scan.status,
345
+ createdAt: bundle.scan.createdAt,
346
+ completedAt: bundle.scan.completedAt,
347
+ producer: bundle.scan.producer,
348
+ findingCount: bundle.findings.length,
349
+ target: bundle.scan.target,
350
+ });
351
+ }
352
+ return summaries;
353
+ }
354
+
355
+ async getFinding(scanId: string, findingId: string): Promise<SecurityFinding | null> {
356
+ const bundle = await this.getBundle(scanId);
357
+ return bundle?.findings.find(finding => finding.id === findingId) ?? null;
358
+ }
359
+
360
+ async updateDisposition(
361
+ scanId: string,
362
+ findingId: string,
363
+ disposition: SecurityDisposition,
364
+ ): Promise<SecurityFinding> {
365
+ return withSecurityStoreWrite(this.#projectDirectory, async () => {
366
+ const bundle = await this.#getBundleUnlocked(scanId);
367
+ if (!bundle) throw new Error(`Unknown security scan: ${scanId}`);
368
+ const index = bundle.findings.findIndex(finding => finding.id === findingId);
369
+ if (index < 0) throw new Error(`Unknown security finding: ${findingId}`);
370
+ const canonicalDisposition: SecurityDisposition = { status: disposition.status };
371
+ if (disposition.rationale !== undefined) canonicalDisposition.rationale = disposition.rationale;
372
+ if (disposition.updatedAt !== undefined) canonicalDisposition.updatedAt = disposition.updatedAt;
373
+ if (disposition.actor !== undefined) canonicalDisposition.actor = disposition.actor;
374
+ const updated = { ...bundle.findings[index], disposition: canonicalDisposition };
375
+ bundle.findings[index] = parseSecurityFinding(updated);
376
+ if (bundle.sarif !== undefined) bundle.sarif = exportSecurityBundleToSarif(bundle);
377
+ await this.#putBundleUnlocked(bundle);
378
+ return bundle.findings[index];
379
+ });
380
+ }
381
+
382
+ async updateValidation(
383
+ scanId: string,
384
+ findingId: string,
385
+ validation: SecurityValidation,
386
+ evidence: readonly SecurityEvidence[] = [],
387
+ ): Promise<SecurityFinding> {
388
+ return withSecurityStoreWrite(this.#projectDirectory, async () => {
389
+ const bundle = await this.#getBundleUnlocked(scanId);
390
+ if (!bundle) throw new Error(`Unknown security scan: ${scanId}`);
391
+ const index = bundle.findings.findIndex(finding => finding.id === findingId);
392
+ if (index < 0) throw new Error(`Unknown security finding: ${findingId}`);
393
+ const finding = bundle.findings[index];
394
+ const evidenceById = new Map(finding.evidence.map(item => [item.id, item]));
395
+ for (const item of evidence) evidenceById.set(item.id, item);
396
+ const canonicalValidation: SecurityValidation = {
397
+ status: validation.status,
398
+ evidenceIds: [...new Set(validation.evidenceIds)],
399
+ };
400
+ if (validation.summary !== undefined) canonicalValidation.summary = validation.summary;
401
+ if (validation.validatedAt !== undefined) canonicalValidation.validatedAt = validation.validatedAt;
402
+ for (const evidenceId of canonicalValidation.evidenceIds) {
403
+ if (!evidenceById.has(evidenceId)) {
404
+ throw new Error(`Unknown security validation evidence: ${evidenceId}`);
405
+ }
406
+ }
407
+ bundle.findings[index] = parseSecurityFinding({
408
+ ...finding,
409
+ evidence: [...evidenceById.values()],
410
+ validation: canonicalValidation,
411
+ });
412
+ if (bundle.sarif !== undefined) bundle.sarif = exportSecurityBundleToSarif(bundle);
413
+ await this.#putBundleUnlocked(bundle);
414
+ return bundle.findings[index];
415
+ });
416
+ }
417
+
418
+ async compare(beforeScanId: string, afterScanId: string): Promise<SecurityComparisonReport> {
419
+ const before = await this.getBundle(beforeScanId);
420
+ const after = await this.getBundle(afterScanId);
421
+ if (!before) throw new Error(`Unknown security scan: ${beforeScanId}`);
422
+ if (!after) throw new Error(`Unknown security scan: ${afterScanId}`);
423
+ return compareSecurityLineage(before, after);
424
+ }
425
+
426
+ async storeDigest(): Promise<string> {
427
+ const index = await this.#readIndex();
428
+ return Bun.SHA256.hash(JSON.stringify(index), "hex");
429
+ }
430
+ }
@@ -47,6 +47,7 @@ export type AgentSessionEvent =
47
47
  }
48
48
  | { type: "retry_fallback_applied"; from: string; to: string; role: string }
49
49
  | { type: "retry_fallback_succeeded"; model: string; role: string }
50
+ | { type: "model_changed" }
50
51
  | { type: "ttsr_triggered"; rules: Rule[] }
51
52
  | { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
52
53
  | { type: "todo_auto_clear" }
@@ -1,4 +1,11 @@
1
- import type { Agent, AgentMessage, AgentTool, StreamFn, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
+ import type {
2
+ Agent,
3
+ AgentMessage,
4
+ AgentTool,
5
+ AgentToolContext,
6
+ StreamFn,
7
+ ThinkingLevel,
8
+ } from "@oh-my-pi/pi-agent-core";
2
9
  import type {
3
10
  Context,
4
11
  Effort,
@@ -17,6 +24,7 @@ import type { AsyncJob, AsyncJobDeliveryState, AsyncJobManager } from "../async"
17
24
  import type { ModelRegistry } from "../config/model-registry";
18
25
  import type { PromptTemplate } from "../config/prompt-templates";
19
26
  import type { Settings, SkillsSettings } from "../config/settings";
27
+ import type { CursorMcpResourceAdapter } from "../cursor";
20
28
  import type { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
21
29
  import type { TtsrManager } from "../export/ttsr";
22
30
  import type { LoadedCustomCommand } from "../extensibility/custom-commands";
@@ -161,6 +169,8 @@ export interface AgentSessionConfig {
161
169
  sideStreamFn?: StreamFn;
162
170
  /** Stream wrapper for advisor requests. */
163
171
  advisorStreamFn?: StreamFn;
172
+ /** Advisor spend already recorded for the session being opened, restored on resume. */
173
+ initialAdvisorCosts?: ReadonlyMap<string, number>;
164
174
  /** Prefer websocket transport for OpenAI Codex requests when supported. */
165
175
  preferWebsockets?: boolean;
166
176
  /** Provider payload hook used by the active session request path. */
@@ -207,6 +217,35 @@ export interface AgentSessionConfig {
207
217
  providerPromptCacheKeySource?: "explicit" | "fork";
208
218
  /** Full advisor toolset built against an advisor-scoped tool session. */
209
219
  advisorTools?: AgentTool[];
220
+ /**
221
+ * Build a `grep` honoring a Cursor `pi_grep` frame's own context width and
222
+ * match cap, against the advisor-scoped tool session. Without it an advisor
223
+ * running on Cursor silently drops both fields.
224
+ */
225
+ advisorCreateGrepTool?(options: { context?: number; totalMatchLimit?: number }): AgentTool | undefined;
226
+ /**
227
+ * Build the `replace`-mode `edit` a Cursor `pi_edit` frame needs, against the
228
+ * advisor-scoped tool session. The advisor's ordinary instance follows the
229
+ * configured `edit.mode` and rejects the frame's `old_text`/`new_text` pairs.
230
+ */
231
+ advisorCreateEditTool?(): AgentTool | undefined;
232
+ /**
233
+ * The execute-time context the advisor's bridge tools resolve approval from.
234
+ *
235
+ * `ExtensionToolWrapper` reads `tools.approvalMode`, per-tool
236
+ * `tools.approval.<tool>` policies and `autoApprove` only from this context;
237
+ * with none it defaults to `yolo` with empty policies, so a bridge tool would
238
+ * run a native frame the user configured `ask` or `deny` for.
239
+ */
240
+ advisorGetToolContext?: () => AgentToolContext | undefined;
241
+ /**
242
+ * The live MCP connections the advisor's Cursor resource frames answer from.
243
+ *
244
+ * Advisors share the session's connections and may be granted tools from
245
+ * those same servers; without this their `list_mcp_resources` reports an
246
+ * empty catalog and every `read_mcp_resource` a `not_found`.
247
+ */
248
+ advisorMcpResources?: CursorMcpResourceAdapter;
210
249
  /** Preloaded watchdog prompt content for the advisor. */
211
250
  advisorWatchdogPrompt?: string;
212
251
  /** Shared advisor instructions loaded from WATCHDOG.yml. */