one-code-extension 0.5.0 → 0.6.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 (95) hide show
  1. package/README.md +1 -1
  2. package/docs/guide/differences-from-claude-code.md +9 -6
  3. package/docs/guide/installation.md +3 -4
  4. package/docs/guide/troubleshooting.md +1 -1
  5. package/extensions/auto-mode/denials.ts +3 -2
  6. package/extensions/background/cron.ts +127 -0
  7. package/extensions/background/index.ts +135 -47
  8. package/extensions/background/loop-fire.ts +39 -2
  9. package/extensions/background/switch-guard.ts +10 -6
  10. package/extensions/background/wakeup.ts +21 -2
  11. package/extensions/bash/index.ts +8 -30
  12. package/extensions/btw/index.ts +32 -18
  13. package/extensions/cache-marks/index.ts +42 -0
  14. package/extensions/claude-context/index.ts +157 -172
  15. package/extensions/compaction/index.ts +11 -8
  16. package/extensions/context-budget/budget.ts +16 -2
  17. package/extensions/context-budget/index.ts +15 -14
  18. package/extensions/context-management/index.ts +11 -28
  19. package/extensions/effort/index.ts +70 -7
  20. package/extensions/file-tracker/index.ts +111 -44
  21. package/extensions/file-tracker/replay.ts +76 -49
  22. package/extensions/file-tracker/restore.ts +12 -9
  23. package/extensions/file-tracker/tracker.ts +14 -1
  24. package/extensions/hooks/index.ts +60 -76
  25. package/extensions/hooks/prompt-context.ts +59 -0
  26. package/extensions/hooks/trust.ts +13 -7
  27. package/extensions/ledger/index.ts +46 -0
  28. package/extensions/ledger/wire.ts +154 -0
  29. package/extensions/lib/agent-cron.ts +17 -1
  30. package/extensions/lib/agent-loader.ts +3 -2
  31. package/extensions/lib/anthropic-auth.ts +98 -0
  32. package/extensions/lib/child-extensions.ts +14 -4
  33. package/extensions/lib/context-facts.ts +75 -56
  34. package/extensions/lib/covered-messages.ts +23 -0
  35. package/extensions/lib/deferred.ts +152 -12
  36. package/extensions/lib/ledger/fold-memo.ts +49 -0
  37. package/extensions/lib/ledger/fold.ts +265 -0
  38. package/extensions/lib/ledger/layout.ts +72 -0
  39. package/extensions/lib/ledger/plan.ts +102 -0
  40. package/extensions/lib/ledger/records.ts +276 -0
  41. package/extensions/lib/ledger/state.ts +47 -0
  42. package/extensions/lib/ledger/validate.ts +141 -0
  43. package/extensions/lib/ledger/wire.ts +632 -0
  44. package/extensions/lib/local-command.ts +24 -6
  45. package/extensions/lib/model-catalog/huggingface.json +5 -1
  46. package/extensions/lib/model-catalog/models-dev.json +10535 -10299
  47. package/extensions/lib/model-catalog/openrouter.json +191 -393
  48. package/extensions/lib/notifications.ts +57 -17
  49. package/extensions/lib/pi-version.ts +25 -8
  50. package/extensions/lib/plan-mode-channels.ts +29 -7
  51. package/extensions/lib/previous-mark.ts +101 -0
  52. package/extensions/lib/process-state.ts +48 -0
  53. package/extensions/lib/process-tree.ts +3 -2
  54. package/extensions/lib/reminders.ts +327 -621
  55. package/extensions/lib/request-replay.ts +186 -78
  56. package/extensions/lib/side-call.ts +11 -0
  57. package/extensions/lib/subagent-handback.ts +0 -1
  58. package/extensions/lib/tool-additions.ts +2 -22
  59. package/extensions/lib/turn-budget-layout.ts +18 -2
  60. package/extensions/lsp/index.ts +6 -8
  61. package/extensions/mcp/announce.ts +63 -98
  62. package/extensions/mcp/index.ts +51 -90
  63. package/extensions/mcp/trust.ts +13 -6
  64. package/extensions/model-default/index.ts +86 -19
  65. package/extensions/permissions/index.ts +157 -48
  66. package/extensions/permissions/modes.ts +5 -0
  67. package/extensions/plan-mode/index.ts +142 -45
  68. package/extensions/plan-mode/run-state.ts +26 -0
  69. package/extensions/plugins/index.ts +4 -1
  70. package/extensions/powershell/index.ts +2 -0
  71. package/extensions/recap/index.ts +11 -14
  72. package/extensions/search-tools/index.ts +2 -0
  73. package/extensions/skill/announce.ts +30 -0
  74. package/extensions/skill/index.ts +65 -48
  75. package/extensions/skill/invoke.ts +0 -67
  76. package/extensions/subagents/agents.ts +30 -8
  77. package/extensions/subagents/fork-cache.ts +9 -3
  78. package/extensions/subagents/index.ts +180 -107
  79. package/extensions/subagents/outcome.ts +3 -0
  80. package/extensions/subagents/parent-mode.ts +50 -0
  81. package/extensions/subagents/pending-claim.ts +18 -1
  82. package/extensions/subagents/runner.ts +29 -2
  83. package/extensions/subagents/runs.ts +5 -0
  84. package/extensions/system-prompt/index.ts +40 -6
  85. package/extensions/system-prompt/template.ts +10 -1
  86. package/extensions/system-reminder/index.ts +174 -202
  87. package/extensions/tool-search/index.ts +241 -167
  88. package/extensions/workflow/agent-session.ts +4 -1
  89. package/extensions/workflow/index.ts +16 -4
  90. package/extensions/workflow/run-manager.ts +4 -0
  91. package/extensions/worktree/index.ts +50 -14
  92. package/package.json +5 -2
  93. package/extensions/lib/context-stack.ts +0 -81
  94. package/extensions/subagents/announce.ts +0 -69
  95. package/extensions/tool-search/announce.ts +0 -58
@@ -27,7 +27,6 @@ import { perCwd } from "../lib/per-cwd.ts";
27
27
  import { bashSpawn, piShellEnv, withChildProcessEnv } from "../lib/shell-spawn.ts";
28
28
  import { registerShellTool } from "../lib/shell-tool.ts";
29
29
  import { type DescriptionForm, followDescriptionForm } from "../lib/tool-variants.ts";
30
- import { CONTEXT_BASELINE_CHANNEL, restoredContext } from "../lib/context-stack.ts";
31
30
  import { PERMISSION_STATUS_CHANNEL, type PermissionStatus } from "../permissions/modes.ts";
32
31
  import { BASH_PARAMS, bashDescription } from "./description.ts";
33
32
  import { bashGuardReason } from "./guards.ts";
@@ -70,23 +69,15 @@ export function bashTool(pi: ExtensionAPI, { announce }: { announce: boolean })
70
69
  const spawnHook = (context: BashSpawnContext): BashSpawnContext => ({ ...context, env: childProcessEnv(context.env) });
71
70
  const base = createBashToolDefinition(process.cwd(), { shellPath, spawnHook });
72
71
 
73
- // The tier follows the model; the permission-mode variant freezes when the
74
- // first request starts. Re-registering on a mode switch changes both the
75
- // description and pi's tool order, losing the entire cached prefix. Mode
76
- // reminders and the live permission gate carry later changes instead.
72
+ // The description follows the tier and the permission mode. It reaches the
73
+ // wire only when a request opens an epoch: inside one the ledger keeps the
74
+ // tools array as recorded (extensions/ledger), so a mode switch never moves
75
+ // the tool or re-caches the prefix; mode reminders and the live permission
76
+ // gate carry the change instead.
77
77
  let form: DescriptionForm = "short";
78
78
  // Tiny's dedicated search tools replace the long form's shell-find advice.
79
79
  let searchTools = false;
80
80
  let autoMode = false;
81
- let liveAutoMode = false;
82
- let modeFrozen = false;
83
- const restoredAutoMode = (): boolean | undefined => {
84
- const baselines = restoredContext(pi.events)?.baselines;
85
- // Older snapshots' last mode chose the last request's description.
86
- const saved = baselines?.["bash-auto-mode"];
87
- if (typeof saved === "boolean") return saved;
88
- return typeof baselines?.["permission-mode"] === "string" ? baselines["permission-mode"] === "auto" : undefined;
89
- };
90
81
  const describe = () => bashDescription(form, autoMode, searchTools);
91
82
  const setDescription = registerShellTool(pi, {
92
83
  name: "bash",
@@ -104,24 +95,11 @@ export function bashTool(pi: ExtensionAPI, { announce }: { announce: boolean })
104
95
  setDescription(describe());
105
96
  });
106
97
  pi.events.on(PERMISSION_STATUS_CHANNEL, (data) => {
107
- liveAutoMode = (data as PermissionStatus).mode === "auto";
108
- if (modeFrozen) return;
109
- // Permissions' startup broadcast precedes this extension's session_start.
110
- // Honor a restored variant even here: a temporary re-registration would
111
- // move the tool despite putting its original description back afterwards.
112
- autoMode = restoredAutoMode() ?? liveAutoMode;
98
+ const next = (data as PermissionStatus).mode === "auto";
99
+ if (next === autoMode) return;
100
+ autoMode = next;
113
101
  setDescription(describe());
114
102
  });
115
- pi.on("session_start", () => {
116
- const saved = restoredAutoMode();
117
- modeFrozen = saved !== undefined;
118
- autoMode = saved ?? liveAutoMode;
119
- setDescription(describe());
120
- });
121
- pi.on("turn_start", () => {
122
- modeFrozen = true;
123
- pi.events.emit(CONTEXT_BASELINE_CHANNEL, { key: "bash-auto-mode", value: autoMode });
124
- });
125
103
 
126
104
  // The user's own `!` commands, under the bundled app only: pi runs them with
127
105
  // its full process environment, so they get the same restore. Outside the app
@@ -14,8 +14,8 @@
14
14
  * reads the cache the session wrote (lib/request-replay.ts; the compaction
15
15
  * extension publishes the capture). Without a capture for the session model,
16
16
  * or on a provider API the replay does not cover, it falls back to a
17
- * standalone call: the messages the session last sent (captured from the
18
- * `context` event), the earlier side exchanges, an empty system prompt,
17
+ * standalone call: the current branch's messages (read from the session at
18
+ * call time), the earlier side exchanges, an empty system prompt,
19
19
  * name-only tool stubs so a history carrying tool_use blocks stays valid on
20
20
  * strict providers, and withReasoningFallback for models that cannot disable
21
21
  * thinking. Both run on the SESSION model: a side question deserves the same
@@ -23,17 +23,18 @@
23
23
  *
24
24
  * Like Claude Code, the session keeps a side history: each answered question
25
25
  * is threaded into the next one and listed in the panel, where it can be
26
- * browsed and cleared. `f` forks the current answer into a background fork
26
+ * browsed and cleared. A /tree keeps only the exchanges asked from a point
27
+ * the selected branch holds. `f` forks the current answer into a background fork
27
28
  * subagent through the subagents extension (lib/btw-fork.ts).
28
29
  */
29
30
 
30
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
31
31
  import type { Api, Model } from "@earendil-works/pi-ai";
32
32
  import {
33
33
  convertToLlm,
34
34
  copyToClipboard,
35
35
  type ExtensionAPI,
36
36
  type ExtensionCommandContext,
37
+ type ExtensionContext,
37
38
  getAgentDir,
38
39
  SettingsManager,
39
40
  UserMessageComponent,
@@ -43,7 +44,7 @@ import { btwForkedLine, type BtwForkResult, requestBtwFork } from "../lib/btw-fo
43
44
  import { canShowCustomUi, notifyOrPrint, printAnswer } from "../lib/headless-output.ts";
44
45
  import { announcePromptOptions } from "../lib/prompt-options.ts";
45
46
  import { followLastExchange, replaySideCall } from "../lib/replay-call.ts";
46
- import { answerText, stripImageBlocks, toolStubs, trimToTurnBoundary, withoutSystemMessages } from "../lib/side-call.ts";
47
+ import { answerText, branchMessages, stripImageBlocks, toolStubs, trimToTurnBoundary, withoutSystemMessages } from "../lib/side-call.ts";
47
48
  import { boundedDockHeight, linesComponent, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
48
49
  import { CONVERSATION_CACHE_PLACEMENT } from "../lib/side-call-cache.ts";
49
50
  import { releaseSideCalls, runSideCall } from "../lib/side-call-run.ts";
@@ -88,18 +89,23 @@ export default function btwExtension(pi: ExtensionAPI) {
88
89
  return linesComponent((width) => [...row.render(width), truncateLine(` ⎿ ${paint("dim", btwForkedLine(data.name, data.taskId))}`, width)]);
89
90
  });
90
91
 
91
- // The exact request messages the session last sent (after every extension's
92
- // mutations), captured like recap and the compaction extension do.
93
- let capturedMessages: AgentMessage[] | undefined;
94
- pi.on("context", (event) => {
95
- capturedMessages = event.messages;
96
- });
97
- // The same request as it went on the wire, and the reply that answered it.
92
+ // The session's last request as it went on the wire, and the reply that answered it.
98
93
  const exchange = followLastExchange(pi);
99
94
 
100
95
  // The session's answered side questions, oldest first: sent before each new
101
- // question and listed in the panel. `x` in the panel clears them.
102
- let history: BtwExchange[] = [];
96
+ // question and listed in the panel. `x` in the panel clears them. Each one
97
+ // keeps the entry the conversation stood at when it was asked, since it was
98
+ // answered from the conversation up to there.
99
+ let history: Array<BtwExchange & { askedAt: string | undefined }> = [];
100
+ /** The entry the branch ends at: where a side question is asked from (undefined in an empty session). */
101
+ const leafOf = (ctx: ExtensionContext): string | undefined => ctx.sessionManager.getBranch().at(-1)?.id;
102
+ /** Whether the selected branch holds the point a side question was asked from. */
103
+ const onBranch = (ctx: ExtensionContext, askedAt: string | undefined): boolean =>
104
+ askedAt === undefined || ctx.sessionManager.getBranch().some((entry) => entry.id === askedAt);
105
+ /** Thread an answer into the side history, unless a /tree since left the point it was asked from. */
106
+ const keep = (ctx: ExtensionContext, exchange: BtwExchange, askedAt: string | undefined) => {
107
+ if (exchange.answer && onBranch(ctx, askedAt)) history.push({ ...exchange, askedAt });
108
+ };
103
109
 
104
110
  // The controller for an open panel's in-flight call. Aborted when the session
105
111
  // is replaced or torn down, so a late resolve cannot repaint a disposed `tui`
@@ -116,7 +122,6 @@ export default function btwExtension(pi: ExtensionAPI) {
116
122
  sessionEpoch++;
117
123
  // Claude Code's `[question]` placeholder after a bare `/btw` in the prompt.
118
124
  pi.events.emit(ARGUMENT_HINT_CHANNEL, { command: "btw", hint: "[question]" } satisfies ArgumentHint);
119
- capturedMessages = undefined;
120
125
  history = [];
121
126
  inFlight?.abort();
122
127
  inFlight = undefined;
@@ -127,6 +132,12 @@ export default function btwExtension(pi: ExtensionAPI) {
127
132
  inFlight = undefined;
128
133
  releaseSideCalls(ctx.sessionManager.getSessionId());
129
134
  });
135
+ // A /tree keeps the side exchanges asked from a point the selected branch
136
+ // holds; the others were answered from the abandoned branch's conversation.
137
+ pi.on("session_tree", (_event, ctx) => {
138
+ const ids = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
139
+ history = history.filter((exchange) => exchange.askedAt === undefined || ids.has(exchange.askedAt));
140
+ });
130
141
 
131
142
  // The answer renders as Markdown, like the transcript; plain wrapped text
132
143
  // until the renderer loads, or if it cannot.
@@ -157,7 +168,9 @@ export default function btwExtension(pi: ExtensionAPI) {
157
168
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
158
169
  if (!auth.ok) throw new Error("No API key is available for the current model.");
159
170
  const tools = toolStubs(pi.getActiveTools(), STUB_REASON);
160
- const context = trimToTurnBoundary(withoutSystemMessages(capturedMessages ?? []));
171
+ // The current branch, read now: a /tree or a compaction since the last
172
+ // request changed it, and pi fires no `context` event for either.
173
+ const context = trimToTurnBoundary(withoutSystemMessages(branchMessages(ctx)));
161
174
  // btw runs on a cheap, possibly text-only reader and answers a text question,
162
175
  // so images the conversation carried are stripped before the call
163
176
  // (working-docs/decisions/model-policy.md).
@@ -194,6 +207,7 @@ export default function btwExtension(pi: ExtensionAPI) {
194
207
  ctx.ui.notify("Ask a side question: /btw <question>", "info");
195
208
  return;
196
209
  }
210
+ const askedAt = leafOf(ctx);
197
211
  // The panel can fork a background agent whose completion opens the
198
212
  // session's first turn; this context carries the options that turn
199
213
  // is built from (lib/prompt-options.ts).
@@ -208,7 +222,7 @@ export default function btwExtension(pi: ExtensionAPI) {
208
222
  try {
209
223
  const answer = await ask(ctx, question, history, controller.signal);
210
224
  if (controller.signal.aborted) return;
211
- if (answer) history.push({ question, answer });
225
+ keep(ctx, { question, answer }, askedAt);
212
226
  if (ctx.mode === "rpc") ctx.ui.notify(answer || "(no answer)", "info");
213
227
  else printAnswer(ctx, answer || "(no answer)");
214
228
  } catch (error) {
@@ -259,7 +273,7 @@ export default function btwExtension(pi: ExtensionAPI) {
259
273
  const answer = await ask(ctx, question, earlier, controller.signal);
260
274
  if (controller.signal.aborted) return;
261
275
  view.answer = answer;
262
- if (answer) history.push({ question, answer });
276
+ keep(ctx, { question, answer }, askedAt);
263
277
  } catch (error) {
264
278
  if (controller.signal.aborted) return;
265
279
  view.error = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * cache-marks extension — keeps the previous request's conversation breakpoint
3
+ * on an Anthropic request, so a request that adds many blocks at once still
4
+ * reads the cache the previous one wrote (lib/previous-mark.ts has the why).
5
+ *
6
+ * The previous breakpoint comes from the ledger's plan, not from memory: inside
7
+ * an epoch the request extends a recorded base, and the base carried its tail
8
+ * mark on the last block before its trailing directive. So a resume, /reload,
9
+ * /tree and a pi cache warm mark the same block the base did.
10
+ *
11
+ * Load order: after the ledger's payload hook (extensions/ledger/wire.ts), the
12
+ * last extension that edits the messages. First-party Anthropic only: the
13
+ * Messages API is where the look-back was measured, and its four-breakpoint
14
+ * limit is checked here. `CC_CACHE_MARKS=0` turns it off, to compare a run with
15
+ * and without the mark.
16
+ */
17
+
18
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
19
+ import { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
20
+ import { isFirstPartyAnthropic } from "../lib/anthropic-server-tools.ts";
21
+ import { LEDGER_PLAN_CHANNEL, type RequestPlan } from "../lib/ledger/plan.ts";
22
+ import { baseMark, withPreviousMark } from "../lib/previous-mark.ts";
23
+
24
+ export default function cacheMarksExtension(pi: ExtensionAPI) {
25
+ if (process.env.CC_CACHE_MARKS === "0") return;
26
+ /** The plan of the request being sent; a warm reuses it. */
27
+ let plan: RequestPlan | undefined;
28
+ pi.events.on(LEDGER_PLAN_CHANNEL, (data) => {
29
+ plan = data as RequestPlan;
30
+ });
31
+ pi.on("session_start", () => {
32
+ plan = undefined;
33
+ });
34
+
35
+ pi.on("before_provider_request", (event, ctx) => {
36
+ const payload = event.payload as Record<string, unknown>;
37
+ if (!ctx.model || !isFirstPartyAnthropic(ctx.model) || !looksLikeAnthropicRequest(payload)) return undefined;
38
+ const base = plan && !plan.epoch ? plan.base : undefined;
39
+ const next = withPreviousMark(payload, base ? baseMark(payload.messages, base.list, ctx.model.api) : undefined);
40
+ return next === payload ? undefined : next;
41
+ });
42
+ }