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
@@ -143,10 +143,6 @@ export function missingArgumentsNote(argumentHint: string): { before: string; af
143
143
  };
144
144
  }
145
145
 
146
- /** Fast pre-check before the regex walk; also the marker `redactOffSkillText` scans for. */
147
- const SKILL_BLOCK_START = '<skill name="';
148
- const SKILL_BLOCK_RE = /<skill name="([^"]+)"[^>]*>[\s\S]*?<\/skill>/g;
149
-
150
146
  /**
151
147
  * The hidden message a `/skill:` command is delivered as. `details.input` is
152
148
  * the text the user typed, so an extension that saw that text at `input` (the
@@ -160,69 +156,6 @@ export interface SkillInvocationDetails {
160
156
  input?: string;
161
157
  }
162
158
 
163
- export const offSkillNotice = (name: string): string =>
164
- `[Skill "${name}" is turned off — its instructions were removed. The user can re-enable it from /skills or /plugins.]`;
165
-
166
- /**
167
- * Replace every `<skill>` block belonging to a turned-off skill with a short
168
- * refusal notice. Returns the rewritten text, or undefined when nothing
169
- * needed redacting (so callers keep the original object untouched).
170
- *
171
- * This is the fail-closed backstop behind the `input`-hook interception:
172
- * pi's `steer()`/`followUp()` expand `/skill:<name>` natively WITHOUT firing
173
- * the `input` event (queued interactive messages after the first, RPC
174
- * steer/followUp), so an off skill's instructions can land in the session
175
- * history. Redacting on the `context` hook strips them from every outgoing
176
- * request — the session file keeps the original bytes, and the rewrite is
177
- * deterministic for a fixed override state, so the request prefix stays
178
- * byte-stable across turns (prompt-cache friendly).
179
- */
180
- export function redactOffSkillText(text: string, isOff: (name: string) => boolean): string | undefined {
181
- if (!text.includes(SKILL_BLOCK_START)) return undefined;
182
- let changed = false;
183
- const redacted = text.replace(SKILL_BLOCK_RE, (block, name: string) => {
184
- if (!isOff(name)) return block;
185
- changed = true;
186
- return offSkillNotice(name);
187
- });
188
- return changed ? redacted : undefined;
189
- }
190
-
191
- /**
192
- * Apply redactOffSkillText across a request's messages (user and custom roles,
193
- * string or text-block content). Returns a rewritten copy, or undefined when
194
- * no message needed redacting — callers must then keep the original array so
195
- * untouched requests stay byte-identical.
196
- */
197
- export function redactOffSkillMessages<M extends { role: string; content?: unknown }>(
198
- messages: readonly M[],
199
- isOff: (name: string) => boolean,
200
- ): M[] | undefined {
201
- let changed = false;
202
- const out = messages.map((message) => {
203
- if (message.role !== "user" && message.role !== "custom") return message;
204
- if (typeof message.content === "string") {
205
- const redacted = redactOffSkillText(message.content, isOff);
206
- if (redacted === undefined) return message;
207
- changed = true;
208
- return { ...message, content: redacted };
209
- }
210
- if (!Array.isArray(message.content)) return message;
211
- let blockChanged = false;
212
- const blocks = message.content.map((block: { type?: string; text?: string }) => {
213
- if (block?.type !== "text" || typeof block.text !== "string") return block;
214
- const redacted = redactOffSkillText(block.text, isOff);
215
- if (redacted === undefined) return block;
216
- blockChanged = true;
217
- return { ...block, text: redacted };
218
- });
219
- if (!blockChanged) return message;
220
- changed = true;
221
- return { ...message, content: blocks };
222
- });
223
- return changed ? out : undefined;
224
- }
225
-
226
159
  // ---------------------------------------------------------------------------
227
160
  // Bare `/<skill>` commands (Claude Code's invocation shape)
228
161
 
@@ -14,7 +14,7 @@
14
14
  * Subdirectories are searched recursively.
15
15
  */
16
16
 
17
- import { readdirSync, readFileSync, statSync } from "node:fs";
17
+ import { readdirSync, readFileSync, type Stats, statSync } from "node:fs";
18
18
  import { basename, dirname, join } from "node:path";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { parseFrontmatterLoosely } from "../lib/frontmatter.ts";
@@ -42,32 +42,54 @@ export interface AgentDefinition {
42
42
  source: string;
43
43
  }
44
44
 
45
- function collectMarkdownFiles(dir: string, out: string[] = []): string[] {
45
+ /** Visit every `.md` file under `dir`, at any depth, with its stats. */
46
+ function walkMarkdownFiles(dir: string, visit: (path: string, stats: Stats) => void): void {
46
47
  let entries: string[];
47
48
  try {
48
49
  entries = readdirSync(dir);
49
50
  } catch {
50
- return out; // missing or unreadable directory: nothing to discover
51
+ return; // missing or unreadable directory: nothing to discover
51
52
  }
52
53
  for (const entry of entries) {
53
54
  const full = join(dir, entry);
54
55
  // A dangling symlink or an unreadable entry must not take the whole
55
56
  // catalog (and every Agent call) down with it — skip it (review L1).
56
- let isDirectory: boolean;
57
+ let stats: Stats;
57
58
  try {
58
- isDirectory = statSync(full).isDirectory();
59
+ stats = statSync(full);
59
60
  } catch {
60
61
  continue;
61
62
  }
62
- if (isDirectory) {
63
- collectMarkdownFiles(full, out);
63
+ if (stats.isDirectory()) {
64
+ walkMarkdownFiles(full, visit);
64
65
  } else if (entry.endsWith(".md")) {
65
- out.push(full);
66
+ visit(full, stats);
66
67
  }
67
68
  }
69
+ }
70
+
71
+ function collectMarkdownFiles(dir: string): string[] {
72
+ const out: string[] = [];
73
+ walkMarkdownFiles(dir, (path) => out.push(path));
68
74
  return out;
69
75
  }
70
76
 
77
+ /**
78
+ * A stamp of the agent files `discoverAgents` would read from these sources:
79
+ * each source, then each file's path, times and size. It changes when a file
80
+ * is added, removed, renamed or written, so a catalog built from the sources
81
+ * can be reused while the stamp holds, without reading or parsing any file.
82
+ */
83
+ export function agentSourcesStamp(sources: Array<string | AgentSource>): string {
84
+ const parts: string[] = [];
85
+ for (const source of sources) {
86
+ const { dir, namespace } = typeof source === "string" ? { dir: source, namespace: undefined } : source;
87
+ parts.push(`${namespace ?? ""}@${dir}`);
88
+ walkMarkdownFiles(dir, (path, stats) => parts.push(`${path}:${stats.mtimeMs}:${stats.ctimeMs}:${stats.size}`));
89
+ }
90
+ return parts.join("\n");
91
+ }
92
+
71
93
  /**
72
94
  * Split one frontmatter tool string the way Claude Code does: on commas and
73
95
  * spaces, except inside a rule's parentheses, so `Bash(git push:*), Write` is
@@ -26,10 +26,13 @@
26
26
  */
27
27
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
28
28
  import { DEFER_CHANNEL } from "../lib/deferred.ts";
29
- import { type AffinityModel, affinityHeaders, captureMatches, forkOutputRoom, forkRequestPayload, MIN_REPLAY_OUTPUT_TOKENS, type RequestCapture, toolNames, uncoveredMessages } from "../lib/request-replay.ts";
29
+ import { type AffinityModel, affinityHeaders, captureMatches, forkOutputRoom, forkRequestPayload, forkBaselineNames, MIN_REPLAY_OUTPUT_TOKENS, type RequestCapture, uncoveredMessages } from "../lib/request-replay.ts";
30
30
 
31
31
  type ModelRef = { api?: string; provider?: string; id?: string };
32
32
 
33
+ /** The APIs whose fork splice (`forkRequestPayload`) handles the body's shape. */
34
+ const SPLICED_FORK_APIS: ReadonlySet<string> = new Set(["anthropic-messages", "openai-completions", "openai-responses", "azure-openai-responses", "openai-codex-responses"]);
35
+
33
36
  export function forkCacheExtension(capture: RequestCapture) {
34
37
  return (pi: ExtensionAPI) => {
35
38
  // The fork advertises the parent's tools, where SendMessage is deferred
@@ -46,7 +49,10 @@ export function forkCacheExtension(capture: RequestCapture) {
46
49
  pi.on("context_with_system", (event, ctx) => {
47
50
  shaped = false;
48
51
  const covers = capture.covers;
49
- if (!covers || !captureMatches(capture, ctx.model as ModelRef | undefined)) return undefined;
52
+ // Bedrock and Google nest their tools and need joined turns and the
53
+ // fork's own abort signal, which the fork splice does not handle: their
54
+ // forks go out whole instead.
55
+ if (!covers || !SPLICED_FORK_APIS.has(capture.api) || !captureMatches(capture, ctx.model as ModelRef | undefined)) return undefined;
50
56
  const tail = uncoveredMessages(event.messages as { role: string; timestamp?: number }[], covers);
51
57
  if (!tail) return undefined;
52
58
  // Too little output room left after the prefix: send the fork whole
@@ -69,7 +75,7 @@ export function forkCacheExtension(capture: RequestCapture) {
69
75
  if (!shaped || !captureMatches(capture, ctx.model as ModelRef | undefined)) return undefined;
70
76
  shaped = false;
71
77
  const payload = event.payload as Record<string, unknown>;
72
- baseline ??= new Set(toolNames(payload.tools));
78
+ baseline ??= forkBaselineNames(payload, capture.api);
73
79
  return forkRequestPayload(capture, payload, baseline);
74
80
  });
75
81
  };