one-code-extension 0.1.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 (153) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +202 -0
  3. package/agents/explore.md +24 -0
  4. package/agents/general-purpose.md +21 -0
  5. package/agents/plan.md +22 -0
  6. package/docs/decisions/auto-mode.md +526 -0
  7. package/docs/decisions/branding.md +127 -0
  8. package/docs/decisions/code-review-remediation.md +106 -0
  9. package/docs/decisions/compaction.md +140 -0
  10. package/docs/decisions/distribution.md +133 -0
  11. package/docs/decisions/hooks.md +64 -0
  12. package/docs/decisions/lsp.md +58 -0
  13. package/docs/decisions/mcp.md +104 -0
  14. package/docs/decisions/memory-state.md +170 -0
  15. package/docs/decisions/model-policy.md +126 -0
  16. package/docs/decisions/modes.md +178 -0
  17. package/docs/decisions/skills-plugins.md +56 -0
  18. package/docs/decisions/subagents-workflows.md +225 -0
  19. package/docs/decisions/system-prompt.md +98 -0
  20. package/docs/decisions/tools.md +407 -0
  21. package/docs/decisions/tui.md +202 -0
  22. package/docs/decisions.md +148 -0
  23. package/extensions/ask-user/index.ts +124 -0
  24. package/extensions/ask-user/questions.ts +60 -0
  25. package/extensions/auto-mode/actions.ts +78 -0
  26. package/extensions/auto-mode/classifier.ts +388 -0
  27. package/extensions/auto-mode/config.ts +237 -0
  28. package/extensions/auto-mode/decision-log.ts +60 -0
  29. package/extensions/auto-mode/defaults.ts +65 -0
  30. package/extensions/auto-mode/instructions.ts +74 -0
  31. package/extensions/auto-mode/model-picker.ts +225 -0
  32. package/extensions/auto-mode/model-select.ts +178 -0
  33. package/extensions/auto-mode/paths.ts +109 -0
  34. package/extensions/auto-mode/pause.ts +96 -0
  35. package/extensions/auto-mode/prompt.ts +307 -0
  36. package/extensions/auto-mode/rules.ts +96 -0
  37. package/extensions/auto-mode/safety-floor.ts +100 -0
  38. package/extensions/auto-mode/sensitive.ts +137 -0
  39. package/extensions/auto-mode/shell-analysis.ts +702 -0
  40. package/extensions/background/index.ts +405 -0
  41. package/extensions/background/registry.ts +81 -0
  42. package/extensions/background/wakeup.ts +41 -0
  43. package/extensions/bash/background.ts +136 -0
  44. package/extensions/bash/index.ts +149 -0
  45. package/extensions/branding/assistant-marker.ts +53 -0
  46. package/extensions/branding/index.ts +366 -0
  47. package/extensions/branding/prompt-editor.ts +42 -0
  48. package/extensions/branding/prompt-marker.ts +29 -0
  49. package/extensions/branding/startup.ts +152 -0
  50. package/extensions/claude-compat/index.ts +50 -0
  51. package/extensions/claude-context/index.ts +79 -0
  52. package/extensions/compaction/index.ts +231 -0
  53. package/extensions/compaction/prompt.ts +191 -0
  54. package/extensions/context-management/index.ts +147 -0
  55. package/extensions/effort/index.ts +216 -0
  56. package/extensions/effort/slider.ts +253 -0
  57. package/extensions/exit/index.ts +28 -0
  58. package/extensions/file-tracker/index.ts +103 -0
  59. package/extensions/file-tracker/tracker.ts +145 -0
  60. package/extensions/hooks/debug.ts +53 -0
  61. package/extensions/hooks/executor.ts +132 -0
  62. package/extensions/hooks/index.ts +310 -0
  63. package/extensions/hooks/matcher.ts +72 -0
  64. package/extensions/hooks/plugin-hooks.ts +79 -0
  65. package/extensions/hooks/protocol.ts +151 -0
  66. package/extensions/hooks/settings.ts +163 -0
  67. package/extensions/hooks/trust.ts +151 -0
  68. package/extensions/lib/branch-restore.ts +35 -0
  69. package/extensions/lib/claude-context.ts +148 -0
  70. package/extensions/lib/deferred.ts +119 -0
  71. package/extensions/lib/git.ts +14 -0
  72. package/extensions/lib/memory.ts +219 -0
  73. package/extensions/lib/model-policy.ts +436 -0
  74. package/extensions/lib/model-tier.ts +99 -0
  75. package/extensions/lib/notifications.ts +18 -0
  76. package/extensions/lib/paths.ts +26 -0
  77. package/extensions/lib/per-cwd.ts +21 -0
  78. package/extensions/lib/persisted-output.ts +70 -0
  79. package/extensions/lib/pi-version.ts +54 -0
  80. package/extensions/lib/plugins.ts +227 -0
  81. package/extensions/lib/reminders.ts +212 -0
  82. package/extensions/lib/scratchpad.ts +68 -0
  83. package/extensions/lib/tui-render.ts +522 -0
  84. package/extensions/lsp/client.ts +278 -0
  85. package/extensions/lsp/format.ts +46 -0
  86. package/extensions/lsp/index.ts +193 -0
  87. package/extensions/lsp/protocol.ts +64 -0
  88. package/extensions/lsp/servers.ts +79 -0
  89. package/extensions/mcp/client.ts +205 -0
  90. package/extensions/mcp/config.ts +199 -0
  91. package/extensions/mcp/index.ts +373 -0
  92. package/extensions/mcp/schema.ts +161 -0
  93. package/extensions/memory/index.ts +90 -0
  94. package/extensions/notebook/index.ts +77 -0
  95. package/extensions/notebook/notebook.ts +108 -0
  96. package/extensions/permissions/index.ts +876 -0
  97. package/extensions/permissions/matcher.ts +455 -0
  98. package/extensions/permissions/modes.ts +107 -0
  99. package/extensions/permissions/protected-paths.ts +125 -0
  100. package/extensions/permissions/settings.ts +98 -0
  101. package/extensions/plan-mode/index.ts +209 -0
  102. package/extensions/plan-mode/reminder.ts +33 -0
  103. package/extensions/plan-mode/slug.ts +28 -0
  104. package/extensions/plan-mode/viewer.ts +124 -0
  105. package/extensions/plugins/index.ts +115 -0
  106. package/extensions/plugins/template.ts +42 -0
  107. package/extensions/search-tools/index.ts +0 -0
  108. package/extensions/search-tools/tools.ts +24 -0
  109. package/extensions/skill/index.ts +170 -0
  110. package/extensions/subagents/agents.ts +126 -0
  111. package/extensions/subagents/child.ts +414 -0
  112. package/extensions/subagents/default-model.ts +122 -0
  113. package/extensions/subagents/index.ts +1119 -0
  114. package/extensions/subagents/model-select.ts +346 -0
  115. package/extensions/subagents/rpc-turns.ts +123 -0
  116. package/extensions/subagents/runs.ts +93 -0
  117. package/extensions/subagents/usage.ts +53 -0
  118. package/extensions/subagents/worktree.ts +69 -0
  119. package/extensions/system-prompt/environment.ts +35 -0
  120. package/extensions/system-prompt/index.ts +54 -0
  121. package/extensions/system-prompt/template.ts +103 -0
  122. package/extensions/system-prompt/tiers/common.ts +63 -0
  123. package/extensions/system-prompt/tiers/frontier.ts +24 -0
  124. package/extensions/system-prompt/tiers/low.ts +71 -0
  125. package/extensions/system-prompt/tiers/mid.ts +56 -0
  126. package/extensions/system-reminder/index.ts +37 -0
  127. package/extensions/tasks/index.ts +140 -0
  128. package/extensions/tasks/store.ts +174 -0
  129. package/extensions/todo/index.ts +133 -0
  130. package/extensions/tool-search/index.ts +168 -0
  131. package/extensions/tool-style/index.ts +84 -0
  132. package/extensions/web/index.ts +43 -0
  133. package/extensions/web-fetch/extract.ts +139 -0
  134. package/extensions/web-fetch/index.ts +291 -0
  135. package/extensions/web-fetch/summarize.ts +78 -0
  136. package/extensions/workflow/agent-session.ts +368 -0
  137. package/extensions/workflow/globals.ts +251 -0
  138. package/extensions/workflow/index.ts +364 -0
  139. package/extensions/workflow/journal.ts +75 -0
  140. package/extensions/workflow/permission-gate.ts +71 -0
  141. package/extensions/workflow/run-manager.ts +306 -0
  142. package/extensions/workflow/saved-workflows.ts +45 -0
  143. package/extensions/workflow/script-source.ts +201 -0
  144. package/extensions/workflow/types.ts +99 -0
  145. package/extensions/workflow/vm-runtime.ts +31 -0
  146. package/extensions/workflow/widget.ts +56 -0
  147. package/extensions/worktree/index.ts +266 -0
  148. package/extensions/worktree/rewrite.ts +59 -0
  149. package/package.json +101 -0
  150. package/themes/one-code-light.json +76 -0
  151. package/themes/one-code.json +76 -0
  152. package/types/app.d.ts +5 -0
  153. package/types/community-packages.d.ts +12 -0
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Live progress panel for workflow runs, rendered with the plain string[]
3
+ * overload of ctx.ui.setWidget. Re-renders are debounced so a chatty fan-out
4
+ * doesn't thrash the TUI; the widget clears itself once no run is active.
5
+ */
6
+
7
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+ import type { RunHandle, WorkflowRunManager } from "./run-manager.ts";
9
+
10
+ const WIDGET_KEY = "workflow";
11
+ const DEBOUNCE_MS = 250;
12
+ const EVENT_TAIL = 4;
13
+
14
+ export class WorkflowWidget {
15
+ private timer: ReturnType<typeof setTimeout> | undefined;
16
+
17
+ constructor(
18
+ private readonly manager: WorkflowRunManager,
19
+ private readonly getCtx: () => ExtensionContext | undefined,
20
+ ) {}
21
+
22
+ attach(handle: RunHandle): void {
23
+ handle.on("progress", () => this.schedule());
24
+ handle.on("done", () => this.schedule());
25
+ this.schedule();
26
+ }
27
+
28
+ private schedule(): void {
29
+ if (this.timer) return;
30
+ this.timer = setTimeout(() => {
31
+ this.timer = undefined;
32
+ this.render();
33
+ }, DEBOUNCE_MS);
34
+ this.timer.unref?.();
35
+ }
36
+
37
+ private render(): void {
38
+ const ctx = this.getCtx();
39
+ if (!ctx?.hasUI) return;
40
+ const active = this.manager.list().filter((h) => h.status === "running");
41
+ if (active.length === 0) {
42
+ ctx.ui.setWidget(WIDGET_KEY, undefined);
43
+ return;
44
+ }
45
+ const lines: string[] = [];
46
+ for (const handle of active) {
47
+ const state = handle.state;
48
+ const phase = state?.currentPhase();
49
+ const stats = state ? `${state.agentCount()} agents · ${state.outputTokens()} out-tokens` : "starting…";
50
+ lines.push(`⚡ workflow ${handle.meta.name} (${handle.runId}) — ${phase ? `${phase} · ` : ""}${stats}`);
51
+ for (const event of handle.recentEvents.slice(-EVENT_TAIL)) lines.push(` ${event}`);
52
+ }
53
+ lines.push(" /workflows to inspect · /workflows stop <runId> to stop");
54
+ ctx.ui.setWidget(WIDGET_KEY, lines);
55
+ }
56
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * worktree extension — Claude Code's EnterWorktree/ExitWorktree.
3
+ *
4
+ * enter_worktree creates (or switches into) a git worktree under
5
+ * `.claude/worktrees/` and "moves" the session there. pi fixes a session's cwd
6
+ * at creation, so the move is enforced on the tool_call hook: bash commands
7
+ * are prefixed with `cd`, relative paths resolve against the worktree (see
8
+ * rewrite.ts), and an every-turn reminder keeps the model oriented. State
9
+ * rides in tool-result details so a resumed or branched session restores it.
10
+ */
11
+
12
+ import { execFile } from "node:child_process";
13
+ import { randomBytes } from "node:crypto";
14
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { promisify } from "node:util";
17
+ import { StringEnum } from "@earendil-works/pi-ai";
18
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+ import { Type } from "typebox";
20
+ import { restoreLatestDetails } from "../lib/branch-restore.ts";
21
+ import { DEFER_CHANNEL } from "../lib/deferred.ts";
22
+ import { REMINDER_CHANNEL } from "../lib/reminders.ts";
23
+ import { rewriteToolInput, validateWorktreeName } from "./rewrite.ts";
24
+ import { ccToolRenderers } from "../lib/tui-render.ts";
25
+
26
+ const run = promisify(execFile);
27
+ const REMINDER_KEY = "cc-worktree-session";
28
+ const WORKTREE_TOOLS = new Set(["enter_worktree", "exit_worktree"]);
29
+
30
+ interface WorktreeState {
31
+ path: string;
32
+ branch?: string;
33
+ baseCommit?: string;
34
+ createdByUs: boolean;
35
+ originalCwd: string;
36
+ }
37
+
38
+ interface WorktreeDetails {
39
+ /** null = session exited the worktree; undefined = tool did not change state. */
40
+ worktreeState?: WorktreeState | null;
41
+ }
42
+
43
+ async function git(args: string[], cwd: string): Promise<string> {
44
+ const { stdout } = await run("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
45
+ return stdout.trim();
46
+ }
47
+
48
+ async function listWorktreePaths(cwd: string): Promise<string[]> {
49
+ const out = await git(["worktree", "list", "--porcelain"], cwd);
50
+ return out
51
+ .split("\n")
52
+ .filter((line) => line.startsWith("worktree "))
53
+ .map((line) => line.slice("worktree ".length));
54
+ }
55
+
56
+ export default function worktreeExtension(pi: ExtensionAPI) {
57
+ let state: WorktreeState | undefined;
58
+
59
+ const reminderFor = (s: WorktreeState) =>
60
+ `Worktree session active: you are working in the git worktree at ${s.path}` +
61
+ `${s.branch ? ` (branch ${s.branch})` : ""}, not in ${s.originalCwd}. ` +
62
+ "Relative paths and bash commands already run there. Use exit_worktree to leave when the user asks.";
63
+
64
+ const applyState = (next: WorktreeState | undefined) => {
65
+ state = next;
66
+ if (next) {
67
+ pi.events.emit(REMINDER_CHANNEL, { text: reminderFor(next), scope: "every-turn", key: REMINDER_KEY });
68
+ } else {
69
+ pi.events.emit(REMINDER_CHANNEL, { key: REMINDER_KEY, remove: true });
70
+ }
71
+ };
72
+
73
+ const reconstructState = (ctx: ExtensionContext) => {
74
+ const details = restoreLatestDetails<WorktreeDetails>(ctx.sessionManager.getBranch(), WORKTREE_TOOLS, (d) => d?.worktreeState !== undefined);
75
+ let restored = details?.worktreeState ?? undefined;
76
+ if (restored && !existsSync(restored.path)) restored = undefined;
77
+ applyState(restored);
78
+ };
79
+
80
+ pi.on("session_start", (_event, ctx) => reconstructState(ctx));
81
+ pi.on("session_tree", (_event, ctx) => reconstructState(ctx));
82
+
83
+ pi.on("tool_call", (event) => {
84
+ if (!state) return;
85
+ if (["enter_worktree", "exit_worktree", "subagent", "send_message", "workflow"].includes(event.toolName)) return;
86
+ rewriteToolInput(event.toolName, event.input as Record<string, unknown>, state.path);
87
+ });
88
+
89
+ pi.registerTool({
90
+ name: "enter_worktree",
91
+ label: "Enter Worktree",
92
+ ...ccToolRenderers("Enter Worktree"),
93
+ description:
94
+ "Create an isolated git worktree under .claude/worktrees/ (branched from the current HEAD) and switch this session into it — subsequent commands and relative paths run there. Use ONLY when the user or project instructions explicitly ask for a worktree. Pass `name` to name the new worktree, or `path` to switch into an existing worktree instead (mutually exclusive). Leave with exit_worktree.",
95
+ parameters: Type.Object({
96
+ name: Type.Optional(
97
+ Type.String({ description: "Name for a new worktree (segments of letters/digits/._-, max 64 chars). Random if omitted" }),
98
+ ),
99
+ path: Type.Optional(Type.String({ description: "Existing worktree (from `git worktree list`) to switch into instead of creating one" })),
100
+ }),
101
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
102
+ const fail = (text: string) => ({ content: [{ type: "text" as const, text }], details: {} as WorktreeDetails, isError: true });
103
+
104
+ if (params.name && params.path) return fail("Pass either `name` or `path`, not both.");
105
+ // An empty `path` string is falsy, so without this it would fall through
106
+ // to *creating* a new worktree instead of reporting the bad argument —
107
+ // the mirror of the empty-`name` case, which is already rejected below.
108
+ if (params.path !== undefined && params.path.length === 0) {
109
+ return fail("`path` was empty — pass an existing worktree path from `git worktree list`, or omit `path` to create a new worktree.");
110
+ }
111
+
112
+ let repoRoot: string;
113
+ try {
114
+ repoRoot = await git(["rev-parse", "--show-toplevel"], ctx.cwd);
115
+ } catch {
116
+ return fail("enter_worktree needs a git repository; this directory is not one.");
117
+ }
118
+
119
+ if (params.path) {
120
+ const known = await listWorktreePaths(ctx.cwd);
121
+ const target = known.find((p) => p === params.path);
122
+ if (!target) return fail(`${params.path} is not a worktree of this repository. Known worktrees:\n${known.join("\n")}`);
123
+ const next: WorktreeState = { path: target, createdByUs: false, originalCwd: ctx.cwd };
124
+ applyState(next);
125
+ return {
126
+ content: [{ type: "text", text: `Switched into existing worktree ${target}. All work now happens there; exit_worktree returns to ${ctx.cwd}.` }],
127
+ details: { worktreeState: next } satisfies WorktreeDetails,
128
+ };
129
+ }
130
+
131
+ // Any active worktree session blocks creating another — not just ones we
132
+ // created. A session entered via `path` has createdByUs:false, and
133
+ // without gating on `state` alone a create-new call silently abandoned it.
134
+ if (state) {
135
+ return fail(`Already in a worktree session (${state.path}). exit_worktree first, or switch with \`path\` to another worktree.`);
136
+ }
137
+
138
+ const name = params.name ?? `wt-${randomBytes(3).toString("hex")}`;
139
+ const nameError = validateWorktreeName(name);
140
+ if (nameError) return fail(`Invalid worktree name "${name}": ${nameError}`);
141
+
142
+ const worktreesDir = join(repoRoot, ".claude", "worktrees");
143
+ mkdirSync(worktreesDir, { recursive: true });
144
+ // Self-ignoring directory: worktrees never show up as untracked files.
145
+ const ignorePath = join(worktreesDir, ".gitignore");
146
+ if (!existsSync(ignorePath)) writeFileSync(ignorePath, "*\n");
147
+
148
+ const path = join(worktreesDir, name);
149
+ if (existsSync(path)) return fail(`Worktree ${path} already exists — pass it as \`path\` to switch into it.`);
150
+
151
+ const branch = name.replace(/\//g, "-");
152
+ let baseCommit: string;
153
+ try {
154
+ baseCommit = await git(["rev-parse", "HEAD"], ctx.cwd);
155
+ await git(["worktree", "add", "-b", branch, path, "HEAD"], repoRoot);
156
+ } catch (error) {
157
+ return fail(`Could not create worktree: ${(error as Error).message}`);
158
+ }
159
+
160
+ const next: WorktreeState = { path, branch, baseCommit, createdByUs: true, originalCwd: ctx.cwd };
161
+ applyState(next);
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text",
166
+ text: `Created worktree ${path} on branch ${branch} (from HEAD ${baseCommit.slice(0, 8)}). All commands and relative paths now run there; exit_worktree returns to ${ctx.cwd}.`,
167
+ },
168
+ ],
169
+ details: { worktreeState: next } satisfies WorktreeDetails,
170
+ };
171
+ },
172
+ });
173
+
174
+ pi.registerTool({
175
+ name: "exit_worktree",
176
+ label: "Exit Worktree",
177
+ ...ccToolRenderers("Exit Worktree"),
178
+ description:
179
+ 'End the worktree session started by enter_worktree and return to the original directory. action: "keep" leaves the worktree and branch on disk; "remove" deletes both — refused (listing the changes) if there are uncommitted files or commits not on the original branch, unless discard_changes is true. No-op when no worktree session is active.',
180
+ parameters: Type.Object({
181
+ action: StringEnum(["keep", "remove"] as const, { description: '"keep" preserves the worktree on disk; "remove" deletes it and its branch' }),
182
+ discard_changes: Type.Optional(Type.Boolean({ description: "Required true to remove a worktree with uncommitted files or unmerged commits" })),
183
+ }),
184
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
185
+ if (!state) {
186
+ return {
187
+ content: [{ type: "text", text: "No worktree session is active; nothing to exit." }],
188
+ details: {} satisfies WorktreeDetails,
189
+ };
190
+ }
191
+ const current = state;
192
+
193
+ if (params.action === "keep") {
194
+ applyState(undefined);
195
+ return {
196
+ content: [
197
+ {
198
+ type: "text",
199
+ text: `Left worktree session; back in ${current.originalCwd}. The worktree remains at ${current.path}${current.branch ? ` (branch ${current.branch})` : ""} — re-enter it with enter_worktree {path}.`,
200
+ },
201
+ ],
202
+ details: { worktreeState: null } satisfies WorktreeDetails,
203
+ };
204
+ }
205
+
206
+ if (!current.createdByUs) {
207
+ return {
208
+ content: [
209
+ { type: "text", text: `This worktree (${current.path}) was not created by enter_worktree in this session — exit with action: "keep" and remove it manually if intended.` },
210
+ ],
211
+ details: {} satisfies WorktreeDetails,
212
+ isError: true,
213
+ };
214
+ }
215
+
216
+ if (!params.discard_changes) {
217
+ const blockers: string[] = [];
218
+ try {
219
+ const dirty = await git(["status", "--porcelain"], current.path);
220
+ if (dirty) blockers.push(`Uncommitted changes:\n${dirty}`);
221
+ if (current.baseCommit && current.branch) {
222
+ const ahead = await git(["rev-list", "--count", `${current.baseCommit}..${current.branch}`], current.path);
223
+ if (ahead !== "0") blockers.push(`${ahead} commit(s) on ${current.branch} not on the original branch.`);
224
+ }
225
+ } catch {
226
+ blockers.push("Could not verify the worktree is clean.");
227
+ }
228
+ if (blockers.length > 0) {
229
+ return {
230
+ content: [
231
+ {
232
+ type: "text",
233
+ text: `Refusing to remove ${current.path}:\n\n${blockers.join("\n\n")}\n\nConfirm with the user, then re-invoke with discard_changes: true — or exit with action: "keep".`,
234
+ },
235
+ ],
236
+ details: {} satisfies WorktreeDetails,
237
+ isError: true,
238
+ };
239
+ }
240
+ }
241
+
242
+ try {
243
+ await git(["worktree", "remove", "--force", current.path], current.originalCwd);
244
+ if (current.branch) await git(["branch", "-D", current.branch], current.originalCwd);
245
+ } catch (error) {
246
+ return {
247
+ content: [{ type: "text", text: `Could not remove the worktree: ${(error as Error).message}` }],
248
+ details: {} satisfies WorktreeDetails,
249
+ isError: true,
250
+ };
251
+ }
252
+ applyState(undefined);
253
+ return {
254
+ content: [{ type: "text", text: `Removed worktree ${current.path}${current.branch ? ` and branch ${current.branch}` : ""}; back in ${current.originalCwd}.` }],
255
+ details: { worktreeState: null } satisfies WorktreeDetails,
256
+ };
257
+ },
258
+ });
259
+
260
+ for (const [name, keywords] of Object.entries({
261
+ enter_worktree: ["worktree", "isolate", "branch", "checkout"],
262
+ exit_worktree: ["worktree", "leave", "return", "cleanup"],
263
+ })) {
264
+ pi.events.emit(DEFER_CHANNEL, { name, keywords });
265
+ }
266
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Worktree-session input rewriting (pure).
3
+ *
4
+ * pi binds every built-in tool to the cwd captured at session creation, so a
5
+ * live session cannot truly change directory. While a worktree session is
6
+ * active we instead rewrite tool inputs on the tool_call hook: bash commands
7
+ * get a `cd` prefix, and relative paths resolve against the worktree. Absolute
8
+ * paths are left alone — pointing back at the original checkout stays possible
9
+ * and explicit.
10
+ */
11
+
12
+ import { isAbsolute, resolve } from "node:path";
13
+
14
+ /** Tools whose `path` argument is relative to the session cwd. */
15
+ const PATH_TOOLS = new Set(["read", "edit", "write", "notebook_edit", "grep", "find", "ls", "lsp_diagnostics"]);
16
+ /** Of those, the ones where a missing path means "the cwd itself". */
17
+ const DEFAULTS_TO_CWD = new Set(["grep", "find", "ls"]);
18
+
19
+ export function shellQuote(path: string): string {
20
+ return `'${path.replace(/'/g, "'\\''")}'`;
21
+ }
22
+
23
+ /**
24
+ * Key under which a worktree-rewritten bash call preserves the model's original
25
+ * (pre-`cd`-wrapper) command. The permission matcher reads it so allow/ask/deny
26
+ * rules are evaluated against what the model actually asked for, not the
27
+ * `cd '<wt>' && (…)` wrapper (which starts with `cd` and matches no Bash rule).
28
+ * The classifier and safety floor still see the wrapped `input.command`, whose
29
+ * `cd` makes containment resolve inside the worktree — see docs/decisions/worktree.md.
30
+ */
31
+ export const ORIGINAL_COMMAND_KEY = "__ccOriginalCommand";
32
+
33
+ /** Mutates `input` in place so the call runs inside the worktree. */
34
+ export function rewriteToolInput(toolName: string, input: Record<string, unknown>, worktreePath: string): void {
35
+ if (toolName === "bash") {
36
+ if (typeof input.command === "string") {
37
+ input[ORIGINAL_COMMAND_KEY] = input.command;
38
+ input.command = `cd ${shellQuote(worktreePath)} && (${input.command}\n)`;
39
+ }
40
+ return;
41
+ }
42
+ if (!PATH_TOOLS.has(toolName)) return;
43
+
44
+ if (typeof input.path === "string" && input.path.length > 0) {
45
+ if (!isAbsolute(input.path)) input.path = resolve(worktreePath, input.path);
46
+ } else if (DEFAULTS_TO_CWD.has(toolName)) {
47
+ input.path = worktreePath;
48
+ }
49
+ }
50
+
51
+ /** Validates an EnterWorktree name: /-separated segments of [A-Za-z0-9._-], ≤64 chars total. */
52
+ export function validateWorktreeName(name: string): string | undefined {
53
+ if (name.length === 0 || name.length > 64) return "name must be 1-64 characters";
54
+ if (!/^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/.test(name)) {
55
+ return "each /-separated segment may contain only letters, digits, dots, underscores, and dashes";
56
+ }
57
+ if (name.split("/").some((seg) => seg === "." || seg === "..")) return "segments may not be . or ..";
58
+ return undefined;
59
+ }
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "one-code-extension",
3
+ "version": "0.1.0",
4
+ "description": "One Code as a pi package \u2014 the Claude Code experience on your own pi install: ported system prompt, Claude-Code-format permissions, .claude/ compatibility, ToolSearch-style deferred tools, and system-reminder steering. For the bundled app, install `one-code` instead.",
5
+ "keywords": [
6
+ "pi",
7
+ "pi-package",
8
+ "pi-extension",
9
+ "claude-code",
10
+ "coding-agent",
11
+ "mcp",
12
+ "agent"
13
+ ],
14
+ "license": "MIT",
15
+ "engines": {
16
+ "node": ">=22.19"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/IsuruMaduranga/one-code.git"
21
+ },
22
+ "homepage": "https://github.com/IsuruMaduranga/one-code#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/IsuruMaduranga/one-code/issues"
25
+ },
26
+ "type": "module",
27
+ "pi": {
28
+ "extensions": [
29
+ "extensions/branding/index.ts",
30
+ "extensions/claude-compat/index.ts",
31
+ "extensions/system-reminder/index.ts",
32
+ "extensions/claude-context/index.ts",
33
+ "extensions/hooks/index.ts",
34
+ "extensions/tool-search/index.ts",
35
+ "extensions/search-tools/index.ts",
36
+ "extensions/tool-style/index.ts",
37
+ "extensions/plugins/index.ts",
38
+ "extensions/worktree/index.ts",
39
+ "extensions/file-tracker/index.ts",
40
+ "extensions/memory/index.ts",
41
+ "extensions/permissions/index.ts",
42
+ "extensions/ask-user/index.ts",
43
+ "extensions/skill/index.ts",
44
+ "extensions/background/index.ts",
45
+ "extensions/bash/index.ts",
46
+ "extensions/subagents/index.ts",
47
+ "extensions/workflow/index.ts",
48
+ "extensions/effort/index.ts",
49
+ "extensions/exit/index.ts",
50
+ "extensions/todo/index.ts",
51
+ "extensions/tasks/index.ts",
52
+ "extensions/plan-mode/index.ts",
53
+ "extensions/notebook/index.ts",
54
+ "extensions/web/index.ts",
55
+ "extensions/web-fetch/index.ts",
56
+ "extensions/lsp/index.ts",
57
+ "extensions/mcp/index.ts",
58
+ "extensions/context-management/index.ts",
59
+ "extensions/compaction/index.ts",
60
+ "extensions/system-prompt/index.ts"
61
+ ],
62
+ "themes": [
63
+ "./themes"
64
+ ]
65
+ },
66
+ "files": [
67
+ "extensions/",
68
+ "agents/",
69
+ "themes/",
70
+ "types/",
71
+ "README.md",
72
+ "LICENSE",
73
+ "docs/decisions.md",
74
+ "docs/decisions/"
75
+ ],
76
+ "scripts": {
77
+ "typecheck": "tsc --noEmit",
78
+ "test": "vitest run"
79
+ },
80
+ "dependencies": {
81
+ "@modelcontextprotocol/sdk": "^1.30.0",
82
+ "@mozilla/readability": "^0.6.0",
83
+ "acorn": "^8.18.0",
84
+ "linkedom": "^0.18.13",
85
+ "pi-web-search": "^1.3.1",
86
+ "turndown": "^7.2.4",
87
+ "typebox": "^1.3.7"
88
+ },
89
+ "devDependencies": {
90
+ "@earendil-works/pi-agent-core": "^0.84.1",
91
+ "@earendil-works/pi-ai": "^0.84.1",
92
+ "@earendil-works/pi-coding-agent": "^0.84.1",
93
+ "@types/node": "^26.1.2",
94
+ "@types/turndown": "^5.0.6",
95
+ "typescript": "^5.6.0",
96
+ "vitest": "^2.1.0"
97
+ },
98
+ "peerDependencies": {
99
+ "@earendil-works/pi-coding-agent": ">=0.83.0"
100
+ }
101
+ }
@@ -0,0 +1,76 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "one-code-light",
4
+ "vars": {
5
+ "clay": "#c05f38",
6
+ "clayDeep": "#9c4a29",
7
+ "surface": "#f0ebe5",
8
+ "surfaceRaised": "#e4ddd4",
9
+ "toolIdle": "#f2eee9",
10
+ "toolOk": "#e8f0e4",
11
+ "toolBad": "#f7e6e4",
12
+ "gray": 240,
13
+ "grayDim": 245,
14
+ "green": "#4f7a3c",
15
+ "red": "#b8342c",
16
+ "amber": "#9a6b1f",
17
+ "blue": "#33608c",
18
+ "cyan": "#2f6f72",
19
+ "violet": "#7a4f86"
20
+ },
21
+ "colors": {
22
+ "accent": "clay",
23
+ "border": "gray",
24
+ "borderAccent": "clay",
25
+ "borderMuted": "grayDim",
26
+ "success": "green",
27
+ "error": "red",
28
+ "warning": "amber",
29
+ "muted": "gray",
30
+ "dim": "grayDim",
31
+ "text": "",
32
+ "thinkingText": "grayDim",
33
+ "selectedBg": "surface",
34
+ "scrollbarThumb": "surfaceRaised",
35
+ "userMessageBg": "surface",
36
+ "userMessageText": "",
37
+ "customMessageBg": "surface",
38
+ "customMessageText": "",
39
+ "customMessageLabel": "clay",
40
+ "toolPendingBg": "",
41
+ "toolSuccessBg": "",
42
+ "toolErrorBg": "toolBad",
43
+ "toolTitle": "clay",
44
+ "toolOutput": "",
45
+ "mdHeading": "clay",
46
+ "mdLink": "blue",
47
+ "mdLinkUrl": "gray",
48
+ "mdCode": "cyan",
49
+ "mdCodeBlock": "",
50
+ "mdCodeBlockBorder": "grayDim",
51
+ "mdQuote": "gray",
52
+ "mdQuoteBorder": "grayDim",
53
+ "mdHr": "grayDim",
54
+ "mdListBullet": "clay",
55
+ "toolDiffAdded": "green",
56
+ "toolDiffRemoved": "red",
57
+ "toolDiffContext": "gray",
58
+ "syntaxComment": "grayDim",
59
+ "syntaxKeyword": "clay",
60
+ "syntaxFunction": "blue",
61
+ "syntaxVariable": "amber",
62
+ "syntaxString": "green",
63
+ "syntaxNumber": "violet",
64
+ "syntaxType": "cyan",
65
+ "syntaxOperator": "clayDeep",
66
+ "syntaxPunctuation": "gray",
67
+ "thinkingOff": "grayDim",
68
+ "thinkingMinimal": "gray",
69
+ "thinkingLow": "cyan",
70
+ "thinkingMedium": "blue",
71
+ "thinkingHigh": "clay",
72
+ "thinkingXhigh": "red",
73
+ "thinkingMax": "violet",
74
+ "bashMode": "amber"
75
+ }
76
+ }
@@ -0,0 +1,76 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "one-code",
4
+ "vars": {
5
+ "clay": "#d97757",
6
+ "clayDeep": "#b8623f",
7
+ "surface": "#2a2725",
8
+ "surfaceRaised": "#333029",
9
+ "toolIdle": "#26231f",
10
+ "toolOk": "#1f2a20",
11
+ "toolBad": "#2d1f1f",
12
+ "gray": 245,
13
+ "grayDim": 240,
14
+ "green": "#87b070",
15
+ "red": "#e05a52",
16
+ "amber": "#e0a458",
17
+ "blue": "#7ba2c8",
18
+ "cyan": "#8bb5b7",
19
+ "violet": "#b48ead"
20
+ },
21
+ "colors": {
22
+ "accent": "clay",
23
+ "border": "grayDim",
24
+ "borderAccent": "clay",
25
+ "borderMuted": "grayDim",
26
+ "success": "green",
27
+ "error": "red",
28
+ "warning": "amber",
29
+ "muted": "gray",
30
+ "dim": "grayDim",
31
+ "text": "",
32
+ "thinkingText": "grayDim",
33
+ "selectedBg": "surface",
34
+ "scrollbarThumb": "surfaceRaised",
35
+ "userMessageBg": "surface",
36
+ "userMessageText": "",
37
+ "customMessageBg": "surface",
38
+ "customMessageText": "",
39
+ "customMessageLabel": "clay",
40
+ "toolPendingBg": "",
41
+ "toolSuccessBg": "",
42
+ "toolErrorBg": "toolBad",
43
+ "toolTitle": "clay",
44
+ "toolOutput": "",
45
+ "mdHeading": "clay",
46
+ "mdLink": "blue",
47
+ "mdLinkUrl": "gray",
48
+ "mdCode": "cyan",
49
+ "mdCodeBlock": "",
50
+ "mdCodeBlockBorder": "grayDim",
51
+ "mdQuote": "gray",
52
+ "mdQuoteBorder": "grayDim",
53
+ "mdHr": "grayDim",
54
+ "mdListBullet": "clay",
55
+ "toolDiffAdded": "green",
56
+ "toolDiffRemoved": "red",
57
+ "toolDiffContext": "gray",
58
+ "syntaxComment": "grayDim",
59
+ "syntaxKeyword": "clay",
60
+ "syntaxFunction": "blue",
61
+ "syntaxVariable": "amber",
62
+ "syntaxString": "green",
63
+ "syntaxNumber": "violet",
64
+ "syntaxType": "cyan",
65
+ "syntaxOperator": "clayDeep",
66
+ "syntaxPunctuation": "gray",
67
+ "thinkingOff": "grayDim",
68
+ "thinkingMinimal": "gray",
69
+ "thinkingLow": "cyan",
70
+ "thinkingMedium": "blue",
71
+ "thinkingHigh": "clay",
72
+ "thinkingXhigh": "red",
73
+ "thinkingMax": "violet",
74
+ "bashMode": "amber"
75
+ }
76
+ }
package/types/app.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** Types for the bundled app's plain-JS modules (app/), used by unit tests. */
2
+ declare module "*/app/update-check.mjs" {
3
+ export function isNewerVersion(candidate: string, current: string): boolean;
4
+ export function createUpdateCheck(options: { currentVersion: string; upgradeHint: string }): (pi: unknown) => void;
5
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Type stub for the one bundled community pi package. It ships raw TypeScript
3
+ * written against a different pi version, so typechecking its sources against
4
+ * our pinned pi types produces spurious errors. tsconfig `paths` points the
5
+ * compiler here, while jiti resolves the real module at runtime.
6
+ */
7
+
8
+ declare module "pi-web-search/src/index.ts" {
9
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ const factory: (pi: ExtensionAPI) => unknown;
11
+ export default factory;
12
+ }