one-code-extension 0.1.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +222 -177
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +128 -0
  8. package/docs/guide/configuration.md +173 -0
  9. package/docs/guide/differences-from-claude-code.md +152 -0
  10. package/docs/guide/doctor.md +88 -0
  11. package/docs/guide/hooks.md +111 -0
  12. package/docs/guide/installation.md +126 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
  14. package/docs/guide/providers-and-models.md +196 -0
  15. package/docs/guide/reference.md +226 -0
  16. package/docs/guide/sessions-and-context.md +192 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +178 -0
  18. package/docs/guide/subagents-and-workflows.md +195 -0
  19. package/docs/guide/tasks-and-background-work.md +113 -0
  20. package/docs/guide/terminal-interface.md +157 -0
  21. package/docs/guide/tools.md +177 -0
  22. package/docs/guide/troubleshooting.md +177 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -217
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. package/extensions/workflow/permission-gate.ts +0 -71
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Favorited plugins/skills for the /plugins panel (pure fs, no pi imports).
3
+ *
4
+ * `<plugin root>/favorites.json`: `{plugins: string[], skills: string[]}`.
5
+ * Favorited rows float to the top of the Installed tab.
6
+ */
7
+
8
+ import { join } from "node:path";
9
+ import { readJsonFile, writeJsonAtomic } from "./atomic-write.ts";
10
+
11
+ const FILE = "favorites.json";
12
+
13
+ export interface Favorites {
14
+ plugins: string[];
15
+ skills: string[];
16
+ }
17
+
18
+ export type FavoriteKind = "plugin" | "skill";
19
+
20
+ function strings(value: unknown): string[] {
21
+ return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
22
+ }
23
+
24
+ export function readFavorites(root: string): Favorites {
25
+ const parsed = readJsonFile<{ plugins?: unknown; skills?: unknown }>(join(root, FILE));
26
+ return { plugins: strings(parsed?.plugins), skills: strings(parsed?.skills) };
27
+ }
28
+
29
+ /** Toggle; returns the new favorited state. */
30
+ export function toggleFavorite(root: string, kind: FavoriteKind, key: string): boolean {
31
+ const path = join(root, FILE);
32
+ const existing = readJsonFile<Record<string, unknown>>(path) ?? {};
33
+ const current: Favorites = { plugins: strings(existing.plugins), skills: strings(existing.skills) };
34
+ const list = kind === "plugin" ? current.plugins : current.skills;
35
+ const favorited = !list.includes(key);
36
+ const next = favorited ? [...list, key] : list.filter((k) => k !== key);
37
+ writeJsonAtomic(path, {
38
+ ...existing,
39
+ plugins: kind === "plugin" ? next : current.plugins,
40
+ skills: kind === "skill" ? next : current.skills,
41
+ });
42
+ return favorited;
43
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * fetch with a hard timeout (pure, no pi imports) — the AbortController +
3
+ * unref'd timer boilerplate the plugin-marketplace fetches share.
4
+ */
5
+
6
+ export async function fetchWithTimeout(url: string, timeoutMs: number, init?: RequestInit): Promise<Response> {
7
+ const controller = new AbortController();
8
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
9
+ timer.unref?.();
10
+ try {
11
+ return await fetch(url, { ...init, signal: controller.signal });
12
+ } finally {
13
+ clearTimeout(timer);
14
+ }
15
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Frontmatter parsing that never throws the file away. pi's `parseFrontmatter`
3
+ * is strict YAML and throws on the kind of thing agent/skill/command authors
4
+ * write by hand (an unquoted colon in a description, a stray tab); a bare
5
+ * `catch` around it silently dropped the whole file (documented trap #7). The
6
+ * lenient fallback reads simple `key: value` lines so the file still loads with
7
+ * whatever could be salvaged. Shared by agents, skills and plugin commands.
8
+ */
9
+
10
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
11
+
12
+ export function parseFrontmatterLoosely(content: string): { frontmatter: Record<string, unknown>; body: string } {
13
+ try {
14
+ const parsed = parseFrontmatter(content) as { frontmatter?: Record<string, unknown>; body: string };
15
+ return { frontmatter: parsed.frontmatter ?? {}, body: parsed.body };
16
+ } catch {
17
+ // Fall through to the lenient path.
18
+ }
19
+
20
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
21
+ if (!match) return { frontmatter: {}, body: content };
22
+
23
+ const frontmatter: Record<string, unknown> = {};
24
+ for (const line of match[1].split(/\r?\n/)) {
25
+ const keyValue = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
26
+ if (!keyValue) continue;
27
+ const value = keyValue[2].trim().replace(/^["']|["']$/g, "");
28
+ if (value) frontmatter[keyValue[1]] = value;
29
+ }
30
+ return { frontmatter, body: match[2] };
31
+ }
@@ -1,8 +1,15 @@
1
1
  /** Shared git helpers — pure functions only (safe to import across extensions). */
2
2
 
3
- import { existsSync } from "node:fs";
4
- import { dirname, join } from "node:path";
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
5
 
6
+ /**
7
+ * The nearest checkout root at or above `startDir`: the directory holding a
8
+ * `.git` entry (a directory for a main checkout, a file for a linked worktree
9
+ * or a submodule). This is the bound for walking up a checkout (context files,
10
+ * banner listing); for the repository a directory *belongs to*, which linked
11
+ * worktrees share with their main checkout, use `findProjectRoot`.
12
+ */
6
13
  export function findGitRoot(startDir: string): string | undefined {
7
14
  let dir = startDir;
8
15
  while (true) {
@@ -12,3 +19,38 @@ export function findGitRoot(startDir: string): string | undefined {
12
19
  dir = parent;
13
20
  }
14
21
  }
22
+
23
+ /**
24
+ * The repository `startDir` belongs to — the checkout root, except that a
25
+ * linked worktree (`git worktree add`, including a subagent's isolation
26
+ * worktree in a tmpdir) resolves to its MAIN checkout, so every worktree of one
27
+ * repository shares one project identity (auto-memory dir, per-repo settings,
28
+ * project trust). A submodule keeps its own root. Pure filesystem: the linked
29
+ * worktree's `.git` is a file reading `gitdir: <main>/.git/worktrees/<name>`.
30
+ */
31
+ export function findProjectRoot(startDir: string): string | undefined {
32
+ const root = findGitRoot(startDir);
33
+ if (!root) return undefined;
34
+ return linkedWorktreeMainRoot(root) ?? root;
35
+ }
36
+
37
+ /** For a linked worktree root, the main checkout it belongs to; undefined otherwise. Exported for tests. */
38
+ export function linkedWorktreeMainRoot(root: string): string | undefined {
39
+ const dotGit = join(root, ".git");
40
+ let gitdir: string;
41
+ try {
42
+ if (!statSync(dotGit).isFile()) return undefined;
43
+ const match = /^gitdir:\s*(.+?)\s*$/m.exec(readFileSync(dotGit, "utf-8"));
44
+ if (!match) return undefined;
45
+ gitdir = match[1];
46
+ } catch {
47
+ return undefined;
48
+ }
49
+ const absolute = isAbsolute(gitdir) ? gitdir : resolve(root, gitdir);
50
+ // <main>/.git/worktrees/<name> → <main>. A submodule's gitdir points into
51
+ // <super>/.git/modules/<name> and is left alone.
52
+ const worktrees = dirname(absolute);
53
+ const mainDotGit = dirname(worktrees);
54
+ if (basename(worktrees) !== "worktrees" || basename(mainDotGit) !== ".git") return undefined;
55
+ return dirname(mainDotGit);
56
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Hook gate for in-process agent sessions (pure), shared by the subagent and
3
+ * workflow runners — the child half of `hooks/subagent-bridge.ts`.
4
+ *
5
+ * A child session is built with `noExtensions: true`, so the `hooks` extension
6
+ * never loads inside it. This inline extension forwards the child's
7
+ * `tool_call` / `tool_result` to the parent's hooks extension over the bridge
8
+ * and applies what comes back exactly as the parent applies it to its own
9
+ * calls: a PreToolUse block denies the call with the hook's reason; an
10
+ * updatedInput is applied IN PLACE so the guards after this one (worktree
11
+ * isolation, the permission gate) judge the rewritten input, as Claude Code
12
+ * does; PreToolUse context is queued on the child's reminder channel (the child
13
+ * loads system-reminder), so it lands inside the tool result; PostToolUse
14
+ * block/replacement/context are written into the result content.
15
+ *
16
+ * Fail OPEN, like the parent's own dispatch: hooks are the user's automation,
17
+ * not the safety gate — a missing or throwing bridge leaves the call to the
18
+ * permission gate behind it. (The permission bridge, by contrast, fails closed.)
19
+ */
20
+
21
+ import type { InlineExtension } from "@earendil-works/pi-coding-agent";
22
+ import { applyPostToolUseOutcome } from "../hooks/protocol.ts";
23
+ import type { ChildHookCall, HookBridge } from "../hooks/subagent-bridge.ts";
24
+ import { REMINDER_CHANNEL } from "./reminders.ts";
25
+
26
+ /** Tools the runtime itself injects; never run hooks for them. */
27
+ const DEFAULT_INTERNAL_TOOLS = new Set(["structured_output"]);
28
+
29
+ export interface HookGateOptions {
30
+ /** The child's agent type for the payload's `agent_type`, resolved per session id by the runner. */
31
+ agentTypeOf?: (sessionId: string | undefined) => string | undefined;
32
+ neverGate?: Set<string>;
33
+ }
34
+
35
+ export function hookGateFactory(getBridge: () => HookBridge | undefined, options: HookGateOptions = {}): InlineExtension {
36
+ const neverGate = options.neverGate ?? DEFAULT_INTERNAL_TOOLS;
37
+ return {
38
+ name: "agent-hook-gate",
39
+ hidden: true,
40
+ factory: (pi) => {
41
+ const describe = (toolName: string, input: unknown, ctx: { cwd?: string; sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string | undefined } } | undefined, fallbackCwd: string): ChildHookCall => {
42
+ const sessionId = ctx?.sessionManager?.getSessionId?.();
43
+ return {
44
+ toolName,
45
+ input: (input ?? {}) as Record<string, unknown>,
46
+ cwd: ctx?.cwd ?? fallbackCwd,
47
+ sessionId,
48
+ transcriptPath: ctx?.sessionManager?.getSessionFile?.() ?? undefined,
49
+ agentType: options.agentTypeOf?.(sessionId),
50
+ };
51
+ };
52
+
53
+ pi.on("tool_call", async (event, ctx) => {
54
+ if (neverGate.has(event.toolName)) return undefined;
55
+ try {
56
+ const bridge = getBridge();
57
+ if (!bridge) return undefined;
58
+ const call = describe(event.toolName, event.input, ctx as never, process.cwd());
59
+ const outcome = await bridge.preToolUse(call);
60
+ if (outcome.block) return { block: true, reason: `PreToolUse hook: ${outcome.block.reason}` };
61
+ if (outcome.updatedInput) Object.assign(event.input as Record<string, unknown>, outcome.updatedInput);
62
+ if (outcome.additionalContext) pi.events.emit(REMINDER_CHANNEL, { text: outcome.additionalContext });
63
+ } catch {
64
+ // Fail open: a broken hook pipeline must never take the child's turn down.
65
+ }
66
+ return undefined;
67
+ });
68
+
69
+ pi.on("tool_result", async (event, ctx) => {
70
+ if (neverGate.has(event.toolName)) return undefined;
71
+ try {
72
+ const bridge = getBridge();
73
+ if (!bridge) return undefined;
74
+ const outcome = await bridge.postToolUse({
75
+ ...describe(event.toolName, event.input, ctx as never, process.cwd()),
76
+ content: event.content,
77
+ isError: event.isError,
78
+ });
79
+ // Same assembly as the parent's tool_result handler; the parent already
80
+ // framed additionalContext as a <system-reminder>.
81
+ const content = applyPostToolUseOutcome(event.content, outcome);
82
+ if (!content) return undefined;
83
+ return { content, isError: event.isError };
84
+ } catch {
85
+ return undefined;
86
+ }
87
+ });
88
+ },
89
+ };
90
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Read how an agent run ended off its last assistant message.
3
+ *
4
+ * pi marks the assistant message it was streaming with `stopReason: "aborted"`
5
+ * when the user hits Esc (or otherwise calls `ctx.abort()`), and with
6
+ * `stopReason: "error"` when the provider call failed; a run that completed
7
+ * carries "stop"/"toolUse"/etc. instead. Claude Code keys its InterruptedByUser
8
+ * line off the same abort signal (ERROR_MESSAGE_USER_ABORT on the last
9
+ * assistant message). Duck-typed so the extensions that need it can share it
10
+ * without pulling in pi's message types.
11
+ *
12
+ * **One abort is mislabelled.** An abort that lands between a stored tool
13
+ * result and the next stream start (the tool saw the signal and returned
14
+ * "Command aborted"; the loop still has a queued steer to deliver, so it calls
15
+ * the provider with the signal already aborted) comes back as an assistant
16
+ * message with EMPTY content, `stopReason: "error"` and `errorMessage: "This
17
+ * operation was aborted"` — the fetch layer's AbortError, not pi's "aborted"
18
+ * label (measured over RPC on pi 0.85.0, STEERING-REVIEW-2026-09-05 L1;
19
+ * upstream ask docs/upstream_prs.md #18). The run's abort signal is still the
20
+ * ground truth, and `ctx.signal` at `agent_end` is that signal (pi's
21
+ * `activeRun` outlives the event), so callers pass `ctx.signal?.aborted` and
22
+ * an `error` stop on an aborted run reads as "aborted": the user stopped the
23
+ * turn, and everything downstream (the Interrupted line, the notifier's
24
+ * post-abort hold, the Stop-hook skip) treats it as it treats Esc. Matching
25
+ * the error text was rejected: three libraries' wordings, any of which can
26
+ * change under us.
27
+ *
28
+ * **One turn is not one run.** `agent_end` fires per low-level run, and pi
29
+ * continues on its own afterwards in three cases: an auto-retry of a transient
30
+ * provider error, an auto-compaction followed by a retry, and a queued
31
+ * follow-up message. `agent_settled` is the one event meaning pi will not
32
+ * continue by itself, and it carries no messages — so anything that reports a
33
+ * turn as finished listens on `agent_settled` and keeps a `RunOutcomeLatch` to
34
+ * carry the outcome over from the runs' `agent_end`. Findings §3.
35
+ */
36
+
37
+ /** How the last assistant message of a run ended. */
38
+ export type RunOutcome = "ok" | "aborted" | "error";
39
+
40
+ /** The only fields of a run's messages any of this reads. */
41
+ export interface RunMessage {
42
+ role: string;
43
+ stopReason?: string;
44
+ }
45
+
46
+ /**
47
+ * Classify a run from its messages; a run with no assistant message counts as
48
+ * "ok". `signalAborted` is the run's abort signal at `agent_end`
49
+ * (`ctx.signal?.aborted`): it turns an `error` stop into "aborted" (see the
50
+ * header) and leaves a completed stop alone — a signal aborted after the final
51
+ * message landed did not interrupt anything.
52
+ */
53
+ export function runOutcome(messages: ReadonlyArray<RunMessage> | undefined, signalAborted = false): RunOutcome {
54
+ if (!messages) return "ok";
55
+ for (let i = messages.length - 1; i >= 0; i--) {
56
+ const message = messages[i];
57
+ if (message.role !== "assistant") continue;
58
+ if (message.stopReason === "aborted") return "aborted";
59
+ if (message.stopReason === "error") return signalAborted ? "aborted" : "error";
60
+ return "ok";
61
+ }
62
+ return "ok";
63
+ }
64
+
65
+ /** The last assistant message in a turn was aborted by the user. */
66
+ export function wasInterrupted(messages: ReadonlyArray<RunMessage> | undefined, signalAborted = false): boolean {
67
+ return runOutcome(messages, signalAborted) === "aborted";
68
+ }
69
+
70
+ /**
71
+ * Carries the last run's outcome from `agent_end` to `agent_settled`.
72
+ *
73
+ * Empty reads as `undefined` rather than "ok", so "no run ended in this turn"
74
+ * stays distinguishable from "a run finished cleanly". A settle with nothing
75
+ * recorded means the turn produced no response at all, and a caller gating on
76
+ * `=== "ok"` then holds off by construction instead of treating the stale
77
+ * default as success. `take()` empties the latch, so the next turn starts clean
78
+ * whether or not this turn's outcome was read.
79
+ */
80
+ export class RunOutcomeLatch {
81
+ private outcome: RunOutcome | undefined;
82
+
83
+ /** `agent_end`: record how that run ended (pass `ctx.signal?.aborted`), replacing any earlier run's. */
84
+ record(messages: ReadonlyArray<RunMessage> | undefined, signalAborted = false): void {
85
+ this.outcome = runOutcome(messages, signalAborted);
86
+ }
87
+
88
+ /** `agent_settled`: the settled turn's outcome, or undefined if no run ended. */
89
+ take(): RunOutcome | undefined {
90
+ const outcome = this.outcome;
91
+ this.outcome = undefined;
92
+ return outcome;
93
+ }
94
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Persisted "disabled MCP server" overrides (pure-ish — touches only ~/.onecode).
3
+ *
4
+ * The /mcp panel's Disable/Enable action persists across sessions here rather
5
+ * than by mutating the user's `~/.claude.json` or a plugin's `.mcp.json`: those
6
+ * are Claude Code's / a plugin's config, which One Code only ever reads (the
7
+ * "own state, borrowed config" rule — docs/decisions/memory-state.md). A
8
+ * disabled server is still discovered and listed; the mcp extension just skips
9
+ * connecting it, and the panel offers Enable to bring it back.
10
+ *
11
+ * Scope mirrors the settings layout: a server configured in the user file
12
+ * (`~/.claude.json`) disables at user scope so it stays disabled everywhere;
13
+ * anything project/local/plugin disables per-repo so it doesn't leak across
14
+ * projects. Reads merge both scopes.
15
+ */
16
+
17
+ import os from "node:os";
18
+ import { readJsonFile } from "./atomic-write.ts";
19
+ import { oneCodeProjectSettingsPath, oneCodeSettingsPath, readSettingsForWrite, writeSettings } from "./one-code-settings.ts";
20
+
21
+ const KEY = "disabledMcpServers";
22
+
23
+ export type McpDisableScope = "user" | "project";
24
+
25
+ function stringArray(value: unknown): string[] {
26
+ return Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
27
+ }
28
+
29
+ function namesAt(path: string): string[] {
30
+ // Lenient on the READ path: a malformed settings file must not abort MCP
31
+ // startup (review M11). The write path below stays strict so it never
32
+ // clobbers a file it could not parse.
33
+ return stringArray(readJsonFile<Record<string, unknown>>(path)?.[KEY]);
34
+ }
35
+
36
+ /** Server names disabled at either scope, merged. */
37
+ export function readDisabledMcpServers(
38
+ cwd: string,
39
+ home: string = os.homedir(),
40
+ env: NodeJS.ProcessEnv = process.env,
41
+ ): Set<string> {
42
+ return new Set([...namesAt(oneCodeSettingsPath(home, env)), ...namesAt(oneCodeProjectSettingsPath(cwd, home, env))]);
43
+ }
44
+
45
+ /** Add or remove a server from the disabled list at the given scope. */
46
+ export function setMcpServerDisabled(
47
+ name: string,
48
+ disabled: boolean,
49
+ scope: McpDisableScope,
50
+ cwd: string,
51
+ home: string = os.homedir(),
52
+ env: NodeJS.ProcessEnv = process.env,
53
+ ): void {
54
+ const path = scope === "user" ? oneCodeSettingsPath(home, env) : oneCodeProjectSettingsPath(cwd, home, env);
55
+ const file = readSettingsForWrite(path);
56
+ const set = new Set(stringArray(file[KEY]));
57
+ if (disabled) set.add(name);
58
+ else set.delete(name);
59
+ file[KEY] = [...set].sort();
60
+ writeSettings(path, file);
61
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Channel the mcp extension uses to publish its live tool definitions so the
3
+ * subagent runner can inject them into in-process child sessions as customTools —
4
+ * the child then reaches MCP servers through the parent's already-open
5
+ * connections instead of connecting its own (Claude Code's shared-services model).
6
+ *
7
+ * The definitions' execute() closes over the parent mcp extension's live
8
+ * connection map, so they must only ever be used in the same process.
9
+ */
10
+
11
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
12
+
13
+ export const MCP_TOOLS_CHANNEL = "one-code:mcp-tools";
14
+
15
+ export interface McpToolsPayload {
16
+ /** Every MCP tool currently registered on the parent session (re-published as servers connect). */
17
+ tools: ToolDefinition[];
18
+ /**
19
+ * True on the final publish, when every configured server has connected or
20
+ * failed (also emitted with zero tools when no servers are configured).
21
+ * Consumers spawning children early can wait for this instead of snapshotting
22
+ * a still-connecting set.
23
+ */
24
+ settled?: boolean;
25
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Cross-extension MCP status channel (pure types/constants, no pi imports).
3
+ *
4
+ * jiti gives every extension file its own module instance, so the /plugins
5
+ * panel cannot import the mcp extension's connection state directly. Instead:
6
+ * the panel emits MCP_STATUS_REQUEST_CHANNEL, and the mcp extension replies on
7
+ * MCP_STATUS_CHANNEL with a snapshot (same data its /mcp command formats).
8
+ * The reply is synchronous and in-process, but the request/reply shape also
9
+ * covers the ordering problem — the bus does not replay, and the panel opens
10
+ * long after the mcp extension's connect events fired.
11
+ */
12
+
13
+ export const MCP_STATUS_REQUEST_CHANNEL = "one-code:mcp-status-request";
14
+ export const MCP_STATUS_CHANNEL = "one-code:mcp-status";
15
+
16
+ export type McpStatusKind = "connected" | "failed" | "authNeeded" | "connecting" | "disabled";
17
+
18
+ export interface McpServerStatus {
19
+ name: string;
20
+ status: McpStatusKind;
21
+ /** Failure reason / missing env var, for the row's detail text. */
22
+ detail?: string;
23
+ toolCount?: number;
24
+ /** Set when a plugin's .mcp.json contributed this server. */
25
+ pluginName?: string;
26
+ /** Where the server was configured (".mcp.json", "~/.claude.json", plugin path…). */
27
+ source?: string;
28
+ }
29
+
30
+ export interface McpStatusEvent {
31
+ servers: McpServerStatus[];
32
+ /** False while the startup connect is still in flight. */
33
+ settled: boolean;
34
+ }
@@ -18,7 +18,8 @@
18
18
 
19
19
  import os from "node:os";
20
20
  import { join } from "node:path";
21
- import { findGitRoot } from "./git.ts";
21
+ import { findProjectRoot } from "./git.ts";
22
+ import { claudeUserDir } from "./paths.ts";
22
23
 
23
24
  /** Claude Code's project-directory slug: every char outside [A-Za-z0-9-] becomes "-". */
24
25
  export function projectSlug(projectRoot: string): string {
@@ -31,16 +32,18 @@ export function projectSlug(projectRoot: string): string {
31
32
  * worktrees and subdirectories share one memory directory), else the cwd.
32
33
  */
33
34
  export function memoryDir(home: string, projectRoot: string): string {
34
- return join(home, ".claude", "projects", projectSlug(projectRoot), "memory");
35
+ return join(claudeUserDir(home), "projects", projectSlug(projectRoot), "memory");
35
36
  }
36
37
 
37
38
  /**
38
39
  * `memoryDir` composed with the actual project-root resolution every caller
39
- * needs: the git repository root when there is one (shared by worktrees and
40
- * subdirectories), else `cwd` itself. `home` defaults to `os.homedir()`.
40
+ * needs: the git repository root when there is one — a linked worktree
41
+ * (including a subagent's isolation worktree) resolves to its main checkout, so
42
+ * worktrees and subdirectories share one directory — else `cwd` itself. `home`
43
+ * defaults to `os.homedir()`.
41
44
  */
42
45
  export function projectMemoryDir(cwd: string, home: string = os.homedir()): string {
43
- return memoryDir(home, findGitRoot(cwd) ?? cwd);
46
+ return memoryDir(home, findProjectRoot(cwd) ?? cwd);
44
47
  }
45
48
 
46
49
  /**
@@ -94,6 +97,38 @@ export const INDEX_NEAR_LIMIT_REMINDER = `MEMORY.md is approaching its load limi
94
97
 
95
98
  export const INDEX_OVER_LIMIT_ERROR = `MEMORY.md is over its load limit (${INDEX_MAX_LINES} lines / 25KB): everything past the limit is dropped the next time it is loaded. The write succeeded, but rewrite the index now — one line per entry, move detail into topic files, merge or drop stale entries.`;
96
99
 
100
+ /**
101
+ * Claude Code's soft size limit for a context file (CLAUDE.md/AGENTS.md/ONECODE.md):
102
+ * over ~40k chars it warns at startup that the file is bloating the context, and
103
+ * points at `/memory` to trim it. Distinct from the MEMORY.md *index* load limit
104
+ * above — this is about the instruction files the model reads every turn.
105
+ */
106
+ export const CLAUDE_MD_CHAR_LIMIT = 40_000;
107
+
108
+ /**
109
+ * CC's over-limit warning for a context file, or null when within the limit.
110
+ * `name` is the file's basename (e.g. `CLAUDE.md`); `chars` is its length. The
111
+ * `N.Nk` rendering matches CC's "over the 40.0k-char limit (55.4k chars)".
112
+ */
113
+ export function claudeMdLimitWarning(name: string, chars: number): string | null {
114
+ if (chars <= CLAUDE_MD_CHAR_LIMIT) return null;
115
+ const k = (n: number) => `${(n / 1000).toFixed(1)}k`;
116
+ return `${name} is over the ${k(CLAUDE_MD_CHAR_LIMIT)}-char limit (${k(chars)} chars) · /memory to free up context`;
117
+ }
118
+
119
+ /**
120
+ * The combined-size warning: when several instruction files each fit but together
121
+ * blow the budget. `totalChars` is the sum across every instruction file One Code
122
+ * sends (CLAUDE.md family / AGENTS.md fallback / ONECODE.md). Returns null within
123
+ * the limit. The caller suppresses this when a single file already tripped
124
+ * `claudeMdLimitWarning`, so a lone bloated file is named once, not twice.
125
+ */
126
+ export function combinedLimitWarning(totalChars: number): string | null {
127
+ if (totalChars <= CLAUDE_MD_CHAR_LIMIT) return null;
128
+ const k = (n: number) => `${(n / 1000).toFixed(1)}k`;
129
+ return `Project instructions total ${k(totalChars)} chars, over the ${k(CLAUDE_MD_CHAR_LIMIT)}-char limit · /memory to free up context`;
130
+ }
131
+
97
132
  /**
98
133
  * Claude Code stamps bookkeeping fields into a memory file's frontmatter at
99
134
  * write time: node_type, the writing session's id, and a modified timestamp
@@ -136,7 +171,7 @@ export function stampFrontmatter(content: string, sessionId: string, modifiedIso
136
171
  * tier), so the prompt stays byte-stable across turns for a given cwd.
137
172
  *
138
173
  * `verbose` selects the long, explicit spec (Claude Code's Haiku memory prompt,
139
- * adapted) for the mid/low tiers; frontier gets the compact version.
174
+ * adapted) for the workhorse/cheap/tiny tiers; frontier gets the compact version.
140
175
  */
141
176
  export function memoryPromptSection(dir: string, verbose = false): string {
142
177
  return verbose ? verboseMemorySection(dir) : compactMemorySection(dir);