one-code-extension 0.1.4 → 0.3.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.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
* fail-closed choices are One Code's own (see docs/decisions.md).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isAppendedReminderText } from "../lib/reminders.ts";
|
|
9
|
+
|
|
10
|
+
/** The contiguous run of reminder/countdown blocks at the tail of a tool result (review M3). */
|
|
11
|
+
function trailingReminderBlocks<T extends { type: string }>(content: readonly T[]): T[] {
|
|
12
|
+
let start = content.length;
|
|
13
|
+
for (let i = content.length - 1; i >= 0; i--) {
|
|
14
|
+
const block = content[i] as { type: string; text?: unknown };
|
|
15
|
+
if (block.type === "text" && typeof block.text === "string" && isAppendedReminderText(block.text)) start = i;
|
|
16
|
+
else break;
|
|
17
|
+
}
|
|
18
|
+
return content.slice(start);
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
export type CcHookEvent =
|
|
9
22
|
| "PreToolUse"
|
|
10
23
|
| "PostToolUse"
|
|
@@ -39,7 +52,16 @@ export interface HookStdinPayload {
|
|
|
39
52
|
prompt?: string;
|
|
40
53
|
stop_hook_active?: boolean;
|
|
41
54
|
trigger?: "manual" | "auto";
|
|
55
|
+
/** PreCompact: the `/compact <instructions>` text, empty when none (Claude Code sends the string as is). */
|
|
56
|
+
custom_instructions?: string;
|
|
57
|
+
/** PostCompact: the summary that replaced the compacted span. */
|
|
58
|
+
compact_summary?: string;
|
|
42
59
|
source?: string;
|
|
60
|
+
/** SessionEnd: why the session ended — clear | logout | prompt_input_exit | other (Claude Code's values). */
|
|
61
|
+
reason?: string;
|
|
62
|
+
/** Set when the call is a subagent's (Claude Code: hooks tell a child's call from the main thread's by its presence). */
|
|
63
|
+
agent_id?: string;
|
|
64
|
+
agent_type?: string;
|
|
43
65
|
}
|
|
44
66
|
|
|
45
67
|
export interface HookEnvelope {
|
|
@@ -149,3 +171,39 @@ export function interpretHookResult(event: CcHookEvent, run: FinishedRun): HookO
|
|
|
149
171
|
if (envelope.systemMessage?.trim()) outcome.systemMessage = envelope.systemMessage.trim();
|
|
150
172
|
return outcome;
|
|
151
173
|
}
|
|
174
|
+
|
|
175
|
+
/** A text block as pi's tool results carry it; what PostToolUse assembly adds. */
|
|
176
|
+
interface TextBlock {
|
|
177
|
+
type: "text";
|
|
178
|
+
text: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A tool result's content after PostToolUse hooks spoke, or undefined when no
|
|
183
|
+
* hook changed anything. Shared by the parent's `tool_result` handler and the
|
|
184
|
+
* child hook gate so a subagent's results are assembled exactly like the
|
|
185
|
+
* parent's: an `updatedToolResult` replaces the content (a non-string is
|
|
186
|
+
* JSON-encoded), a block's reason is put in front — the tool already ran, so
|
|
187
|
+
* the objection is delivered in the result the model reads, the error flag left
|
|
188
|
+
* as the tool set it (a successful write marked as an error invites a redo) —
|
|
189
|
+
* and `additionalContext` (already framed by the caller) goes behind.
|
|
190
|
+
*/
|
|
191
|
+
export function applyPostToolUseOutcome<T extends { type: string }>(
|
|
192
|
+
content: readonly T[],
|
|
193
|
+
outcome: Pick<HookOutcome, "block" | "updatedToolResult" | "additionalContext">,
|
|
194
|
+
): Array<T | TextBlock> | undefined {
|
|
195
|
+
if (!outcome.block && outcome.updatedToolResult === undefined && !outcome.additionalContext) return undefined;
|
|
196
|
+
let result: Array<T | TextBlock> = [...content];
|
|
197
|
+
if (outcome.updatedToolResult !== undefined) {
|
|
198
|
+
const replacement = outcome.updatedToolResult;
|
|
199
|
+
// The reminder queue may already have appended one-shots and the
|
|
200
|
+
// `<total_tokens>` countdown onto this result (system-reminder runs its
|
|
201
|
+
// tool_result hook before ours). Replacing the content must not drop
|
|
202
|
+
// them, so keep the trailing run of reminder blocks (review M3).
|
|
203
|
+
const reminderTail = trailingReminderBlocks(content);
|
|
204
|
+
result = [{ type: "text", text: typeof replacement === "string" ? replacement : JSON.stringify(replacement) }, ...reminderTail];
|
|
205
|
+
}
|
|
206
|
+
if (outcome.block) result = [{ type: "text", text: `PostToolUse hook: ${outcome.block.reason}` }, ...result];
|
|
207
|
+
if (outcome.additionalContext) result = [...result, { type: "text", text: outcome.additionalContext }];
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parent→child hook bridge.
|
|
3
|
+
*
|
|
4
|
+
* In-process subagent sessions get a `noExtensions` loader, so the `hooks`
|
|
5
|
+
* extension never loads inside them, and until 2026-09-05 a user's
|
|
6
|
+
* PreToolUse/PostToolUse hooks — the Claude Code-compatible way to block a
|
|
7
|
+
* command class, rewrite inputs, run a formatter, inject context — applied to
|
|
8
|
+
* the main model's tool calls and to nothing a child did
|
|
9
|
+
* (STEERING-REVIEW-2026-09-05 M4). Claude Code runs hooks inside subagents
|
|
10
|
+
* (`tools/AgentTool/runAgent.ts` passes the same `canUseTool` pipeline; hook
|
|
11
|
+
* payloads carry `agent_id`/`agent_type`).
|
|
12
|
+
*
|
|
13
|
+
* Same transport as the permission bridge (`permissions/subagent-gate.ts`):
|
|
14
|
+
* the parent's `hooks` extension publishes a closure pair on this channel, the
|
|
15
|
+
* spawning extension captures it and threads it into the child loader, whose
|
|
16
|
+
* `hook-gate` inline extension calls it from the child's `tool_call` and
|
|
17
|
+
* `tool_result` handlers. Dispatch happens parent-side — the hook settings,
|
|
18
|
+
* project-hook consent, debug logging and the trust store stay in one place —
|
|
19
|
+
* with the payload naming the child (its session id as `agent_id`, the agent
|
|
20
|
+
* type, its cwd). Only the tool hooks are bridged: UserPromptSubmit, Stop,
|
|
21
|
+
* SessionStart/End and the compaction hooks are main-conversation events
|
|
22
|
+
* (Claude Code fires SubagentStop for a child's end, which One Code does not
|
|
23
|
+
* implement yet).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type BridgeWatchApi, watchBridge } from "../lib/bridge-watch.ts";
|
|
27
|
+
|
|
28
|
+
export const SUBAGENT_HOOK_CHANNEL = "one-code:subagent-hook-bridge";
|
|
29
|
+
|
|
30
|
+
/** One child tool call, as the child's `tool_call` handler sees it. */
|
|
31
|
+
export interface ChildHookCall {
|
|
32
|
+
toolName: string;
|
|
33
|
+
input: Record<string, unknown>;
|
|
34
|
+
/** The child's runtime cwd (a worktree, if isolated). */
|
|
35
|
+
cwd: string;
|
|
36
|
+
/** The child session's id — becomes the payload's `session_id` and `agent_id`. */
|
|
37
|
+
sessionId?: string;
|
|
38
|
+
/** The child's session file, if persisted (payload `transcript_path`). */
|
|
39
|
+
transcriptPath?: string;
|
|
40
|
+
/** The agent type (`explore`, `general-purpose`, …), set by the runner; undefined for a fork. */
|
|
41
|
+
agentType?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The child's tool result, for PostToolUse. */
|
|
45
|
+
export interface ChildHookResult extends ChildHookCall {
|
|
46
|
+
content: unknown;
|
|
47
|
+
isError: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** What the parent's PreToolUse hooks decided; fields absent when no hook spoke. */
|
|
51
|
+
export interface ChildPreToolUseOutcome {
|
|
52
|
+
block?: { reason: string };
|
|
53
|
+
/** Already translated back to the tool's native parameter names. */
|
|
54
|
+
updatedInput?: Record<string, unknown>;
|
|
55
|
+
additionalContext?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What the parent's PostToolUse hooks decided. */
|
|
59
|
+
export interface ChildPostToolUseOutcome {
|
|
60
|
+
block?: { reason: string };
|
|
61
|
+
updatedToolResult?: unknown;
|
|
62
|
+
additionalContext?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface HookBridge {
|
|
66
|
+
preToolUse(call: ChildHookCall): Promise<ChildPreToolUseOutcome>;
|
|
67
|
+
postToolUse(result: ChildHookResult): Promise<ChildPostToolUseOutcome>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SubagentHookPayload {
|
|
71
|
+
bridge: HookBridge;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A lazy getter for the latest published hook bridge (`lib/bridge-watch.ts`); undefined until the hooks extension publishes. */
|
|
75
|
+
export function watchHookBridge(pi: BridgeWatchApi): () => HookBridge | undefined {
|
|
76
|
+
return watchBridge(pi, SUBAGENT_HOOK_CHANNEL, (data) => (data as SubagentHookPayload | undefined)?.bridge);
|
|
77
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Project-hook consent. Hooks in a repo's `.claude/settings.json` are
|
|
3
3
|
* arbitrary code execution the moment the repo is opened, so they run only
|
|
4
4
|
* after the user approves them — once per configuration: approval is keyed to
|
|
5
|
-
* a hash of the project+local hook config and persisted under `~/.
|
|
5
|
+
* a hash of the project+local hook config and persisted under `~/.onecode`, so
|
|
6
6
|
* it survives restarts but any change to the hooks re-prompts (Claude Code's
|
|
7
7
|
* own hooks-review behaviour). User, managed, and plugin hooks never prompt
|
|
8
8
|
* (installing a plugin was the consent).
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { dirname, join } from "node:path";
|
|
21
|
+
import { boundConsentItems } from "../lib/consent-preview.ts";
|
|
21
22
|
import { oneCodeStateDir } from "../lib/paths.ts";
|
|
22
23
|
import type { HooksSource } from "./settings.ts";
|
|
23
24
|
|
|
@@ -84,9 +85,12 @@ export function describeProjectHooks(projectSources: HooksSource[]): string {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
// Every command in full: a truncated command hides its tail, which is exactly
|
|
89
|
+
// where a padded `echo ok …; curl … | sh` would put the malicious part
|
|
90
|
+
// (review M5). But bound the total so a repo with hundreds of hooks (or one
|
|
91
|
+
// megabyte-long command) cannot flood the modal — past the budget the user is
|
|
92
|
+
// pointed at the file itself.
|
|
93
|
+
return `${commands.length} command hook(s):\n${boundConsentItems(commands, "review .claude/settings.json before approving")}`;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
export interface TrustDecisionDeps {
|
|
@@ -94,6 +98,12 @@ export interface TrustDecisionDeps {
|
|
|
94
98
|
confirm: (title: string, message: string) => Promise<boolean | undefined>;
|
|
95
99
|
notify: (message: string) => void;
|
|
96
100
|
storePath?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Answer from the remembered/stored approval only: neither prompt nor record
|
|
103
|
+
* a decision. For a dispatch at session shutdown — recording "declined" there
|
|
104
|
+
* would silence the consent prompt in the session that replaces it.
|
|
105
|
+
*/
|
|
106
|
+
noPrompt?: boolean;
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
/** Process-lifetime memory of declined/approved hashes per project, so a dispatch storm asks once. */
|
|
@@ -120,6 +130,7 @@ export async function projectHooksApproved(
|
|
|
120
130
|
sessionDecisions.set(key, true);
|
|
121
131
|
return true;
|
|
122
132
|
}
|
|
133
|
+
if (deps.noPrompt) return false;
|
|
123
134
|
if (!deps.hasUI) {
|
|
124
135
|
sessionDecisions.set(key, false);
|
|
125
136
|
deps.notify("Project hooks skipped: not yet approved and no UI to ask (user/managed hooks still run).");
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init extension — Claude Code's `/init`.
|
|
3
|
+
*
|
|
4
|
+
* `/init` scans the repo and writes a concise CLAUDE.md for future sessions. It
|
|
5
|
+
* is a prompt template, not procedural code: the handler submits INIT_PROMPT as
|
|
6
|
+
* a user turn (pi.sendUserMessage) and the model does the work with the ordinary
|
|
7
|
+
* tool set — subagent survey, ask_user_question, write. This mirrors how Claude
|
|
8
|
+
* Code's `/init` is just a canned prompt.
|
|
9
|
+
*
|
|
10
|
+
* pi has no built-in `/init`, so a Claude Code user typing it would otherwise
|
|
11
|
+
* have the word go to the model as a chat message.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { INIT_PROMPT } from "./prompt.ts";
|
|
16
|
+
|
|
17
|
+
export default function initExtension(pi: ExtensionAPI) {
|
|
18
|
+
pi.registerCommand("init", {
|
|
19
|
+
description: "Analyze the codebase and set up a CLAUDE.md for future sessions",
|
|
20
|
+
// sendUserMessage always triggers a turn and returns void; the async
|
|
21
|
+
// handler satisfies RegisteredCommand's Promise<void> contract.
|
|
22
|
+
handler: async () => {
|
|
23
|
+
pi.sendUserMessage(INIT_PROMPT, { deliverAs: "followUp" });
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/init` prompt — adapted from Claude Code's `/init`, kept provider-neutral
|
|
3
|
+
* (One Code writes a CLAUDE.md that any Claude Code-compatible agent reads) and
|
|
4
|
+
* self-contained (no dependency on CC-internal skills like `update-config`).
|
|
5
|
+
*
|
|
6
|
+
* Submitted verbatim as a user turn by extensions/init/index.ts; the model does
|
|
7
|
+
* the work with the normal tool set (subagent survey, ask_user_question, write).
|
|
8
|
+
*/
|
|
9
|
+
export const INIT_PROMPT = `Analyze this codebase and set up a concise CLAUDE.md at the repository root, to be loaded into every future session working here. Keep it minimal — every line must earn its place: include only what an agent would get wrong or waste time rediscovering without it.
|
|
10
|
+
|
|
11
|
+
## 1. Survey the codebase
|
|
12
|
+
Launch an Explore subagent (or read directly if the repo is small) to gather:
|
|
13
|
+
- Build, test, and lint commands — especially non-standard scripts, flags, or how to run a *single* test.
|
|
14
|
+
- Languages, frameworks, package manager, and whether this is a monorepo/multi-module or single project.
|
|
15
|
+
- High-level architecture that requires reading several files to grasp (the "big picture", not a file listing).
|
|
16
|
+
- Non-obvious gotchas, required env vars, or workflow quirks.
|
|
17
|
+
- Existing AI-tool config to fold in: an existing CLAUDE.md, AGENTS.md, .cursor/rules or .cursorrules, .github/copilot-instructions.md, .windsurfrules, .clinerules — and the important parts of README.md.
|
|
18
|
+
|
|
19
|
+
## 2. Fill the gaps
|
|
20
|
+
Use ask_user_question only for things the code cannot answer — branch/PR conventions, required setup, testing quirks, communication preferences. Skip anything already obvious from manifests or the README. Do not mark options as "recommended": this is how their team works, not best practices.
|
|
21
|
+
|
|
22
|
+
## 3. Write CLAUDE.md
|
|
23
|
+
Prefix the file with:
|
|
24
|
+
|
|
25
|
+
\`\`\`
|
|
26
|
+
# CLAUDE.md
|
|
27
|
+
|
|
28
|
+
This file gives guidance to AI coding agents (Claude Code and compatible tools) working in this repository.
|
|
29
|
+
\`\`\`
|
|
30
|
+
|
|
31
|
+
Include: build/test/lint commands an agent can't guess; code-style rules that DIFFER from language defaults; testing instructions and quirks; repo etiquette (branch naming, PR/commit conventions); required env vars or setup; non-obvious gotchas and architectural decisions; the important parts of any existing AI-tool config.
|
|
32
|
+
|
|
33
|
+
Exclude: file-by-file structure or component lists (discoverable by reading); standard language conventions; generic advice ("write clean code", "handle errors"); long references or API docs (link them with \`@path/to/file\` import syntax instead of inlining); anything that changes frequently (reference the source with \`@path\`).
|
|
34
|
+
|
|
35
|
+
Be specific — "Use 2-space indentation in TypeScript" beats "format code properly." Do not repeat yourself and do not invent sections like "Common Development Tasks" — only include what you actually found.
|
|
36
|
+
|
|
37
|
+
If a CLAUDE.md already exists: read it, propose concrete improvements as diffs, and explain each. Do NOT silently overwrite. For projects with distinct subdirectories (monorepos), mention that per-directory CLAUDE.md files are loaded automatically when working in those directories, and offer to create them.
|
|
38
|
+
|
|
39
|
+
## 4. Recap
|
|
40
|
+
Summarize which file(s) you wrote and the key points in each. Remind the user this is a starting point to review and tweak, and that they can run \`/init\` again anytime to re-scan.`;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* interrupted extension — Claude Code's "Interrupted · What should One Code do
|
|
3
|
+
* instead?" line, shown when the user aborts a turn (Esc).
|
|
4
|
+
*
|
|
5
|
+
* On agent_end, if the turn's last assistant message was aborted by the user
|
|
6
|
+
* (findings: pi marks it `stopReason: "aborted"`, or `"error"` on a run whose
|
|
7
|
+
* signal is aborted when the abort reached the provider call —
|
|
8
|
+
* `lib/interrupt.ts`), a display-only session entry
|
|
9
|
+
* is appended (`appendEntry` — not part of the LLM context, so the model never
|
|
10
|
+
* sees the note about its own interruption), rendered dim to match CC's
|
|
11
|
+
* InterruptedByUser component. The paired turn-duration line suppresses itself
|
|
12
|
+
* on the same signal, so an interrupted turn shows this note in place of the
|
|
13
|
+
* "Cooked for …" line — CC's behaviour (its turn-duration render is gated on
|
|
14
|
+
* `!aborted`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { wasInterrupted } from "../lib/interrupt.ts";
|
|
19
|
+
import { linesComponent, safeThemePaint } from "../lib/tui-render.ts";
|
|
20
|
+
import { INTERRUPTED_TEXT } from "./line.ts";
|
|
21
|
+
|
|
22
|
+
const ENTRY_TYPE = "one-code:interrupted";
|
|
23
|
+
|
|
24
|
+
export default function interruptedExtension(pi: ExtensionAPI) {
|
|
25
|
+
pi.registerEntryRenderer(ENTRY_TYPE, (_entry, _options, theme) => {
|
|
26
|
+
const paint = safeThemePaint(theme);
|
|
27
|
+
return linesComponent(() => [paint("dim", INTERRUPTED_TEXT)]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
pi.on("agent_end", (event, ctx) => {
|
|
31
|
+
if (!ctx.hasUI) return;
|
|
32
|
+
if (!wasInterrupted(event.messages, ctx.signal?.aborted)) return;
|
|
33
|
+
pi.appendEntry(ENTRY_TYPE);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure pieces of Claude Code's InterruptedByUser line — the dim
|
|
3
|
+
* "Interrupted · What should Claude do instead?" note CC drops in place of the
|
|
4
|
+
* aborted assistant message when the user hits Esc. Rebranded to One Code; no
|
|
5
|
+
* leading mark, matching CC (its InterruptedByUser renders bare dim text).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** The dim interrupted note, One Code's wording of CC's InterruptedByUser. */
|
|
9
|
+
export const INTERRUPTED_TEXT = "Interrupted · What should One Code do instead?";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `onAbort` when `signal` aborts — at once if it already has — and return
|
|
3
|
+
* the remover to call once the awaited work is over, so the listener does not
|
|
4
|
+
* outlive it. The idiom (check `aborted` first, `{ once: true }`, remove in
|
|
5
|
+
* `finally`) was hand-rolled at every tool that awaits something abortable.
|
|
6
|
+
*/
|
|
7
|
+
export function whenAborted(signal: AbortSignal | undefined, onAbort: () => void): () => void {
|
|
8
|
+
if (!signal) return () => {};
|
|
9
|
+
if (signal.aborted) {
|
|
10
|
+
onAbort();
|
|
11
|
+
return () => {};
|
|
12
|
+
}
|
|
13
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
14
|
+
return () => signal.removeEventListener("abort", onAbort);
|
|
15
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared in-process agent wiring: the one place that builds a pi
|
|
3
|
+
* `ModelRuntime` and a `DefaultResourceLoader` for an agent session that runs
|
|
4
|
+
* inside this process. Used by both the workflow runner
|
|
5
|
+
* (`extensions/workflow/agent-session.ts`) and the subagent runner.
|
|
6
|
+
*
|
|
7
|
+
* Loaders are built with `noExtensions: true` so a child never auto-discovers
|
|
8
|
+
* the whole One Code package (which would recurse into the subagent/workflow
|
|
9
|
+
* tools and re-init UI/MCP/LSP). Fidelity is added back deliberately:
|
|
10
|
+
* `permissionGateFactory` is always attached (a child has no permission
|
|
11
|
+
* extension otherwise); callers pass `extraFactories` (e.g. claude-context,
|
|
12
|
+
* file-tracker) and `extraExtensionPaths` (curated stateless tool providers)
|
|
13
|
+
* for what they need. `extensionFactories` and `additionalExtensionPaths` are
|
|
14
|
+
* both loaded even under `noExtensions`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import {
|
|
20
|
+
type AgentSession,
|
|
21
|
+
type CreateAgentSessionOptions,
|
|
22
|
+
createAgentSession,
|
|
23
|
+
DefaultResourceLoader,
|
|
24
|
+
type ExtensionError,
|
|
25
|
+
type InlineExtension,
|
|
26
|
+
ModelRuntime,
|
|
27
|
+
} from "@earendil-works/pi-coding-agent";
|
|
28
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
29
|
+
import type { HookBridge } from "../hooks/subagent-bridge.ts";
|
|
30
|
+
import type { PermissionBridge } from "../permissions/subagent-gate.ts";
|
|
31
|
+
import { withShortCacheRetention } from "./cache-retention.ts";
|
|
32
|
+
import { hookGateFactory } from "./hook-gate.ts";
|
|
33
|
+
import { permissionGateFactory } from "./permission-gate.ts";
|
|
34
|
+
import { worktreeGuardFactory } from "./worktree-isolation.ts";
|
|
35
|
+
|
|
36
|
+
/** The last assistant message's text (the value an agent run returns). */
|
|
37
|
+
export function finalAssistantText(messages: AgentMessage[]): string {
|
|
38
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
39
|
+
const message = messages[i];
|
|
40
|
+
if (message.role !== "assistant") continue;
|
|
41
|
+
const content = (message as { content: unknown }).content;
|
|
42
|
+
if (typeof content === "string") return content;
|
|
43
|
+
if (Array.isArray(content)) {
|
|
44
|
+
const text = content
|
|
45
|
+
.filter((b): b is { type: "text"; text: string } => (b as { type?: string }).type === "text")
|
|
46
|
+
.map((b) => b.text)
|
|
47
|
+
.join("");
|
|
48
|
+
if (text.trim()) return text;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The canonical model/auth runtime, shared across all agents in a run (building
|
|
56
|
+
* one per agent is expensive). Children are tight loops, so it answers every
|
|
57
|
+
* auth with the provider's 5-minute cache TTL rather than the main session's
|
|
58
|
+
* 1-hour one (lib/cache-retention.ts).
|
|
59
|
+
*/
|
|
60
|
+
export async function createSharedModelRuntime(agentDir: string): Promise<ModelRuntime> {
|
|
61
|
+
const runtime = await ModelRuntime.create({
|
|
62
|
+
authPath: join(agentDir, "auth.json"),
|
|
63
|
+
modelsPath: join(agentDir, "models.json"),
|
|
64
|
+
});
|
|
65
|
+
return withShortCacheRetention(runtime);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AgentLoaderOptions {
|
|
69
|
+
cwd: string;
|
|
70
|
+
agentDir: string;
|
|
71
|
+
/** Per-agent system prompt (an agent's own prompt, or a fork's inherited one). */
|
|
72
|
+
systemPrompt?: string;
|
|
73
|
+
/** Inline factories to attach alongside the permission gate (claude-context, file-tracker, injected tools). */
|
|
74
|
+
extraFactories?: InlineExtension[];
|
|
75
|
+
/** CLI-style extension entrypoints to load (curated stateless tool providers). Loaded even under noExtensions. */
|
|
76
|
+
extraExtensionPaths?: string[];
|
|
77
|
+
/** Tool names the permission gate must never gate (runtime-injected tools). */
|
|
78
|
+
neverGate?: Set<string>;
|
|
79
|
+
/**
|
|
80
|
+
* The parent permissions extension's decision closure. When present, the child's
|
|
81
|
+
* permission gate routes every tool call through it (mode inheritance, classifier,
|
|
82
|
+
* prompts bubbled to the user); when absent (headless runs with no publishing
|
|
83
|
+
* parent), the gate uses its fail-closed local fallback. A getter so it can be
|
|
84
|
+
* read lazily — the bridge may not be published yet when the loader is built.
|
|
85
|
+
*/
|
|
86
|
+
getPermissionBridge?: () => PermissionBridge | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* The parent hooks extension's bridge (hooks/subagent-bridge.ts). When present,
|
|
89
|
+
* the user's PreToolUse/PostToolUse hooks run for the child's tool calls, before
|
|
90
|
+
* the guards and the permission gate (Claude Code runs hooks inside subagents).
|
|
91
|
+
* A getter, read per call, like the permission bridge.
|
|
92
|
+
*/
|
|
93
|
+
getHookBridge?: () => HookBridge | undefined;
|
|
94
|
+
/** Resolves a child session id to its agent type, for the hook payload's `agent_type`. */
|
|
95
|
+
agentTypeOf?: (sessionId: string | undefined) => string | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Stop pi from appending CLAUDE.md/AGENTS.md to the child's system prompt. Set
|
|
98
|
+
* by a caller whose child session loads the `claude-context` extension, which
|
|
99
|
+
* injects the `# claudeMd` reminder itself — otherwise the child carried the
|
|
100
|
+
* project instructions twice (review S10).
|
|
101
|
+
*/
|
|
102
|
+
noContextFiles?: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build and `reload()` a resource loader for an in-process agent session.
|
|
107
|
+
*
|
|
108
|
+
* ONE LOADER PER SESSION — never cache one across sessions. `AgentSession.dispose()`
|
|
109
|
+
* invalidates the loader's shared extension runtime, not just the session's
|
|
110
|
+
* runner: from then on every `pi.*` call from those extension instances throws
|
|
111
|
+
* the stale-ctx error and their `pi.events.on` subscriptions are gone, silently
|
|
112
|
+
* (pi's handler wrappers catch the throw, and an SDK session has no error
|
|
113
|
+
* listener). A cached loader therefore broke the second child of the same
|
|
114
|
+
* agent type, every SendMessage resume, every workflow agent after the first,
|
|
115
|
+
* and any sibling still running when one was disposed
|
|
116
|
+
* (LIFECYCLE-REVIEW-2026-09-06 H3, measured). A build costs ~3 ms once the
|
|
117
|
+
* extension modules are in jiti's cache, so there is nothing to save.
|
|
118
|
+
*/
|
|
119
|
+
export async function buildAgentLoader(options: AgentLoaderOptions): Promise<DefaultResourceLoader> {
|
|
120
|
+
const loader = new DefaultResourceLoader({
|
|
121
|
+
cwd: options.cwd,
|
|
122
|
+
agentDir: options.agentDir,
|
|
123
|
+
noExtensions: true,
|
|
124
|
+
...(options.noContextFiles ? { noContextFiles: true } : {}),
|
|
125
|
+
extensionFactories: [
|
|
126
|
+
// Order is load-bearing, mirroring the main session's extension order:
|
|
127
|
+
// hooks first (a hook's updatedInput must be what every guard judges),
|
|
128
|
+
// then the worktree git-isolation guard, then the permission gate.
|
|
129
|
+
...(options.getHookBridge ? [hookGateFactory(options.getHookBridge, { agentTypeOf: options.agentTypeOf, neverGate: options.neverGate })] : []),
|
|
130
|
+
worktreeGuardFactory(options.cwd),
|
|
131
|
+
permissionGateFactory(options.cwd, os.homedir(), options.neverGate, options.getPermissionBridge),
|
|
132
|
+
...(options.extraFactories ?? []),
|
|
133
|
+
],
|
|
134
|
+
...(options.systemPrompt !== undefined ? { systemPrompt: options.systemPrompt } : {}),
|
|
135
|
+
...(options.extraExtensionPaths ? { additionalExtensionPaths: options.extraExtensionPaths } : {}),
|
|
136
|
+
});
|
|
137
|
+
await loader.reload();
|
|
138
|
+
return loader;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface OpenChildSessionOptions {
|
|
142
|
+
/**
|
|
143
|
+
* The loader for this one session: options to build it, or one already built
|
|
144
|
+
* by `buildAgentLoader` and never used by a session (see buildAgentLoader:
|
|
145
|
+
* a disposed session leaves its loader dead).
|
|
146
|
+
*/
|
|
147
|
+
loader: AgentLoaderOptions | DefaultResourceLoader;
|
|
148
|
+
/** Everything else `createAgentSession` takes: cwd, model, tools, customTools, sessionManager, … */
|
|
149
|
+
session: Omit<CreateAgentSessionOptions, "resourceLoader" | "sessionStartEvent">;
|
|
150
|
+
/**
|
|
151
|
+
* Why the child starts — a fresh run, a resume of a persisted child session,
|
|
152
|
+
* or a fork of the parent transcript. Handlers read it the way they read pi's
|
|
153
|
+
* own (file-tracker reconstructs its read state on `resume`/`fork`).
|
|
154
|
+
*/
|
|
155
|
+
startReason?: "startup" | "resume" | "fork";
|
|
156
|
+
/**
|
|
157
|
+
* Where a child extension's handler errors go. An SDK session has no listener
|
|
158
|
+
* of its own, so without this a throwing child handler leaves no trace.
|
|
159
|
+
*/
|
|
160
|
+
onError?: (error: ExtensionError) => void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Build a fresh loader, create the session on it, and START it: `createAgentSession`
|
|
165
|
+
* never emits `session_start` — pi emits it only from `bindExtensions`, which the
|
|
166
|
+
* CLI modes call and an SDK caller must call itself (measured, LIFECYCLE-REVIEW
|
|
167
|
+
* H2; identical in 0.84.1 and 0.85.0). Until this helper existed no child ever
|
|
168
|
+
* saw the event, so every child ran without `# claudeMd` (claude-context), with
|
|
169
|
+
* its deferred tools eager and unlisted (tool-search), without its tier's search
|
|
170
|
+
* tools (search-tools) and without read-state reconstruction on a resume
|
|
171
|
+
* (file-tracker). Binding also runs the child's `resources_discover` pass, the
|
|
172
|
+
* way pi's own modes do.
|
|
173
|
+
*/
|
|
174
|
+
export async function openChildSession(options: OpenChildSessionOptions): Promise<AgentSession> {
|
|
175
|
+
const loader = options.loader instanceof DefaultResourceLoader ? options.loader : await buildAgentLoader(options.loader);
|
|
176
|
+
const { session } = await createAgentSession({
|
|
177
|
+
...options.session,
|
|
178
|
+
resourceLoader: loader,
|
|
179
|
+
sessionStartEvent: { type: "session_start", reason: options.startReason ?? "startup" },
|
|
180
|
+
});
|
|
181
|
+
await session.bindExtensions({ onError: options.onError });
|
|
182
|
+
return session;
|
|
183
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recognising an Anthropic Messages request body inside a `before_provider_request`
|
|
3
|
+
* payload (pure). Shared by context-management (clear_thinking), compaction
|
|
4
|
+
* (its replayed request) and tool-search (deferred tool definitions).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Anthropic Messages API shape: `messages` + `max_tokens`, and not an OpenAI `input`. */
|
|
8
|
+
export function looksLikeAnthropicRequest(payload: unknown): boolean {
|
|
9
|
+
if (!payload || typeof payload !== "object") return false;
|
|
10
|
+
const record = payload as Record<string, unknown>;
|
|
11
|
+
if (!Array.isArray(record.messages)) return false;
|
|
12
|
+
if ("input" in record) return false;
|
|
13
|
+
const model = typeof record.model === "string" ? record.model : "";
|
|
14
|
+
return model.includes("claude") || typeof record.max_tokens === "number";
|
|
15
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic JSON file writes (pure fs, no pi imports).
|
|
3
|
+
*
|
|
4
|
+
* Write to a temp sibling, then rename over the target — a reader never sees a
|
|
5
|
+
* partially written file, and a crash mid-write leaves the target untouched.
|
|
6
|
+
* Read-modify-write callers must spread over the parsed existing object so
|
|
7
|
+
* unknown fields round-trip (the files also belong to future versions and to
|
|
8
|
+
* users who hand-edit them).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
|
|
14
|
+
let sequence = 0;
|
|
15
|
+
|
|
16
|
+
export function writeJsonAtomic(path: string, data: unknown, options: { mode?: number } = {}): void {
|
|
17
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
18
|
+
const tmp = `${path}.tmp-${process.pid}-${++sequence}`;
|
|
19
|
+
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, options.mode !== undefined ? { mode: options.mode } : undefined);
|
|
20
|
+
renameSync(tmp, path);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Parse a JSON file, or `undefined` when missing/unreadable/malformed. */
|
|
24
|
+
export function readJsonFile<T>(path: string): T | undefined {
|
|
25
|
+
let raw: string;
|
|
26
|
+
try {
|
|
27
|
+
raw = readFileSync(path, "utf-8");
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(raw) as T;
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A `{key: boolean}` store file (the shape both override stores use): read
|
|
40
|
+
* drops non-boolean values, write is an atomic read-modify-write that
|
|
41
|
+
* round-trips unknown values.
|
|
42
|
+
*/
|
|
43
|
+
export function readBooleanMap(path: string): Record<string, boolean> {
|
|
44
|
+
const parsed = readJsonFile<Record<string, unknown>>(path);
|
|
45
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
46
|
+
const map: Record<string, boolean> = {};
|
|
47
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
48
|
+
if (typeof value === "boolean") map[key] = value;
|
|
49
|
+
}
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function setBooleanMapEntry(path: string, key: string, value: boolean): void {
|
|
54
|
+
const existing = readJsonFile<Record<string, unknown>>(path) ?? {};
|
|
55
|
+
writeJsonAtomic(path, { ...existing, [key]: value });
|
|
56
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscribe to a bus channel on which a parent extension publishes a bridge
|
|
3
|
+
* (a closure pair a child session calls back into), and return a lazy getter
|
|
4
|
+
* for the latest one. The permission bridge and the hook bridge share this
|
|
5
|
+
* shape: the getter is threaded into child runners and yields `undefined` until
|
|
6
|
+
* the owning extension publishes at session start. Call once from an extension
|
|
7
|
+
* entry point — only they hold a `pi`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface BridgeWatchApi {
|
|
11
|
+
events: { on(channel: string, handler: (data: unknown) => void): void };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function watchBridge<T>(pi: BridgeWatchApi, channel: string, pick: (data: unknown) => T | undefined): () => T | undefined {
|
|
15
|
+
let bridge: T | undefined;
|
|
16
|
+
pi.events.on(channel, (data) => {
|
|
17
|
+
bridge = pick(data);
|
|
18
|
+
});
|
|
19
|
+
return () => bridge;
|
|
20
|
+
}
|