one-code-extension 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +200 -172
  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 +126 -0
  8. package/docs/guide/configuration.md +171 -0
  9. package/docs/guide/differences-from-claude-code.md +150 -0
  10. package/docs/guide/doctor.md +86 -0
  11. package/docs/guide/hooks.md +109 -0
  12. package/docs/guide/installation.md +124 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
  14. package/docs/guide/providers-and-models.md +194 -0
  15. package/docs/guide/reference.md +224 -0
  16. package/docs/guide/sessions-and-context.md +190 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +176 -0
  18. package/docs/guide/subagents-and-workflows.md +193 -0
  19. package/docs/guide/tasks-and-background-work.md +111 -0
  20. package/docs/guide/terminal-interface.md +155 -0
  21. package/docs/guide/tools.md +175 -0
  22. package/docs/guide/troubleshooting.md +175 -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,163 @@
1
+ /**
2
+ * The entries shown in the `/memory` panel and checked by the startup size
3
+ * warning — mirroring Claude Code's picker, extended to the files One Code also
4
+ * loads. Pure (fs reads only), so ordering and labelling are unit-testable.
5
+ *
6
+ * The CLAUDE.md family and ONECODE.md come straight from `discoverContextFilePaths`
7
+ * (the single source of truth for "what's loaded into context"), so this never
8
+ * re-derives discovery or drifts from it. Layered on top, for the picker only:
9
+ * the two always-offered primaries (global + project `CLAUDE.md`, created on
10
+ * save), AGENTS.md **when a loaded file `@`-imports it** (a standalone AGENTS.md
11
+ * is not in our context, so trimming it changes nothing — CC has no AGENTS.md
12
+ * concept at all), and the auto-memory folder as the last entry.
13
+ */
14
+
15
+ import { basename, dirname, join } from "node:path";
16
+ import {
17
+ AGENTS_DESCRIPTOR,
18
+ ancestorDirs,
19
+ collectImportedPaths,
20
+ type ContextFilePath,
21
+ discoverContextFilePaths,
22
+ GLOBAL_DESCRIPTOR,
23
+ LOCAL_DESCRIPTOR,
24
+ ONECODE_DESCRIPTOR,
25
+ ONECODE_GLOBAL_DESCRIPTOR,
26
+ PROJECT_DESCRIPTOR,
27
+ } from "../lib/claude-context.ts";
28
+ import { tryReadFile } from "../lib/plugins.ts";
29
+
30
+ export interface MemoryEntry {
31
+ /** Left-hand label: a friendly role name for known files, else the path. */
32
+ title: string;
33
+ /** Right-hand description, for the CC-known roles. */
34
+ description?: string;
35
+ /** Absolute path the entry opens. */
36
+ path: string;
37
+ kind: "file" | "folder";
38
+ /** File already on disk. The two create-on-open primaries may be false; folders are always true. */
39
+ exists: boolean;
40
+ }
41
+
42
+ /** Display form of a path: a file at the cwd as `./name`, under home as `~/…`, else absolute. */
43
+ function displayPath(path: string, cwd: string, home: string): string {
44
+ if (path.startsWith(`${cwd}/`)) return `./${path.slice(cwd.length + 1)}`;
45
+ if (home && path === home) return "~";
46
+ if (home && path.startsWith(`${home}/`)) return `~${path.slice(home.length)}`;
47
+ return path;
48
+ }
49
+
50
+ /**
51
+ * The paths pulled into context by `@path` imports across the already-discovered
52
+ * context files (read once each). An AGENTS.md shows only if it is in here.
53
+ */
54
+ function referencedImports(discovered: ContextFilePath[], home: string): Set<string> {
55
+ const refs = new Set<string>();
56
+ for (const { path } of discovered) {
57
+ const content = tryReadFile(path);
58
+ if (content === undefined) continue;
59
+ for (const p of collectImportedPaths(content, dirname(path), { home })) refs.add(p);
60
+ }
61
+ return refs;
62
+ }
63
+
64
+ export function buildMemoryEntries(opts: {
65
+ cwd: string;
66
+ home: string;
67
+ homeClaudeDir: string;
68
+ homeOneCodeDir: string;
69
+ memoryDir: string;
70
+ }): MemoryEntry[] {
71
+ const { cwd, home, homeClaudeDir, homeOneCodeDir, memoryDir } = opts;
72
+ // agentsFallback so a directory's AGENTS.md (when it has no CLAUDE.md) is in the
73
+ // list, matching what the # claudeMd block sends.
74
+ const discovered = discoverContextFilePaths({ cwd, homeClaudeDir, homeOneCodeDir, agentsFallback: true });
75
+ const referenced = referencedImports(discovered, home);
76
+ // (descriptor, dir) → the discovered path, so per-dir lookups reuse the real
77
+ // on-disk casing discovery already resolved (no second stat/readdir here).
78
+ const found = new Map<string, string>();
79
+ for (const { path, descriptor } of discovered) found.set(`${descriptor}\0${dirname(path)}`, path);
80
+
81
+ const entries: MemoryEntry[] = [];
82
+ const seen = new Set<string>();
83
+ const add = (entry: MemoryEntry) => {
84
+ if (seen.has(entry.path)) return;
85
+ entries.push(entry);
86
+ seen.add(entry.path);
87
+ };
88
+ const disp = (path: string) => displayPath(path, cwd, home);
89
+
90
+ // Global user instructions — always offered, matching CC (created on save).
91
+ const globalClaude = join(homeClaudeDir, "CLAUDE.md");
92
+ add({
93
+ title: "User instructions",
94
+ description: `Saved in ${disp(globalClaude)}`,
95
+ path: globalClaude,
96
+ kind: "file",
97
+ exists: found.has(`${GLOBAL_DESCRIPTOR}\0${homeClaudeDir}`),
98
+ });
99
+ const globalOneCode = found.get(`${ONECODE_GLOBAL_DESCRIPTOR}\0${homeOneCodeDir}`);
100
+ if (globalOneCode) {
101
+ add({
102
+ title: "One Code user instructions",
103
+ description: `${disp(globalOneCode)} (One Code only)`,
104
+ path: globalOneCode,
105
+ kind: "file",
106
+ exists: true,
107
+ });
108
+ }
109
+
110
+ for (const d of ancestorDirs(cwd)) {
111
+ const isCwd = d === cwd;
112
+ const claude = join(d, "CLAUDE.md");
113
+ if (isCwd) {
114
+ // Project instructions — always offered (created on save).
115
+ add({
116
+ title: "Project instructions",
117
+ description: `Checked in at ${disp(claude)}`,
118
+ path: claude,
119
+ kind: "file",
120
+ exists: found.has(`${PROJECT_DESCRIPTOR}\0${d}`),
121
+ });
122
+ } else if (found.has(`${PROJECT_DESCRIPTOR}\0${d}`)) {
123
+ add({ title: disp(claude), path: claude, kind: "file", exists: true });
124
+ }
125
+
126
+ if (found.has(`${LOCAL_DESCRIPTOR}\0${d}`)) {
127
+ const local = join(d, "CLAUDE.local.md");
128
+ add({ title: disp(local), path: local, kind: "file", exists: true });
129
+ }
130
+
131
+ // AGENTS.md is editable when it's in context: the CLAUDE.md fallback for this
132
+ // directory (in `found`), or `@`-imported into a CLAUDE.md/ONECODE.md.
133
+ const agents = join(d, "AGENTS.md");
134
+ if (found.has(`${AGENTS_DESCRIPTOR}\0${d}`) || referenced.has(agents)) {
135
+ add({
136
+ title: isCwd ? "Agent instructions" : disp(agents),
137
+ description: isCwd ? `Checked in at ${disp(agents)}` : undefined,
138
+ path: agents,
139
+ kind: "file",
140
+ exists: true,
141
+ });
142
+ }
143
+
144
+ const oneCode = found.get(`${ONECODE_DESCRIPTOR}\0${d}`);
145
+ if (oneCode) {
146
+ add({
147
+ title: isCwd ? "One Code instructions" : disp(oneCode),
148
+ description: `${disp(oneCode)} (One Code only)`,
149
+ path: oneCode,
150
+ kind: "file",
151
+ exists: true,
152
+ });
153
+ }
154
+ }
155
+
156
+ add({ title: "Open auto-memory folder", path: memoryDir, kind: "folder", exists: true });
157
+ return entries;
158
+ }
159
+
160
+ /** The basename used in the startup size warning (e.g. `CLAUDE.md`). */
161
+ export function entryName(entry: MemoryEntry): string {
162
+ return basename(entry.path);
163
+ }
@@ -5,6 +5,8 @@
5
5
  * - session start: create the per-project memory directory (the system prompt
6
6
  * promises the model "this directory already exists") and inject the
7
7
  * MEMORY.md index — capped like Claude Code caps it — via the reminder queue.
8
+ * Also raise Claude Code's startup over-limit warning for any loaded context
9
+ * file (CLAUDE.md family, AGENTS.md, ONECODE.md) past the char limit.
8
10
  * - write into the memory dir: stamp bookkeeping frontmatter (node_type,
9
11
  * originSessionId, modified) into the tool input *before* execution, so the
10
12
  * file lands stamped and file-tracker observes the same bytes that are on
@@ -13,19 +15,33 @@
13
15
  * - write/edit of MEMORY.md: check the load limits afterwards; near-limit
14
16
  * nudges over the reminder queue, over-limit turns the result into an error
15
17
  * (the write itself succeeded — the message says so), matching Claude Code.
18
+ * - /memory: Claude Code's Memory picker — a panel of the CLAUDE.md-family /
19
+ * AGENTS.md / ONECODE.md files plus "Open auto-memory folder"; the selection
20
+ * opens in the external $EDITOR/$VISUAL (or the OS opener), as CC does.
16
21
  */
17
22
 
18
23
  import { mkdirSync, readFileSync } from "node:fs";
24
+ import os from "node:os";
19
25
  import { isAbsolute, join, resolve, sep } from "node:path";
20
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
21
27
  import {
28
+ claudeMdLimitWarning,
29
+ combinedLimitWarning,
22
30
  INDEX_NEAR_LIMIT_REMINDER,
23
31
  INDEX_OVER_LIMIT_ERROR,
24
32
  indexLimitStatus,
25
33
  projectMemoryDir,
26
34
  stampFrontmatter,
27
35
  } from "../lib/memory.ts";
36
+ import { claudeConfigDir, oneCodeStateDir } from "../lib/paths.ts";
37
+ import { tryReadFile } from "../lib/plugins.ts";
28
38
  import { REMINDER_CHANNEL } from "../lib/reminders.ts";
39
+ import { boundedDockHeight, safeThemeBold, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
40
+ import { buildMemoryEntries, entryName, type MemoryEntry } from "./entries.ts";
41
+ import { openPath } from "./open-external.ts";
42
+ import { applyMemoryKey, decodeMemoryKey, initialMemoryState, renderMemoryPanel } from "./panel.ts";
43
+
44
+ const MEMORY_PANEL_MAX_HEIGHT = 20;
29
45
 
30
46
  function inputPath(input: unknown, cwd: string): string | undefined {
31
47
  const raw = (input as { path?: unknown })?.path;
@@ -33,13 +49,50 @@ function inputPath(input: unknown, cwd: string): string | undefined {
33
49
  return isAbsolute(raw) ? raw : resolve(cwd, raw);
34
50
  }
35
51
 
52
+ /** All the panel/warning entries for `cwd`, from One Code's real state dirs. */
53
+ function memoryEntriesFor(cwd: string): MemoryEntry[] {
54
+ return buildMemoryEntries({
55
+ cwd,
56
+ home: os.homedir(),
57
+ homeClaudeDir: claudeConfigDir(),
58
+ homeOneCodeDir: oneCodeStateDir(),
59
+ memoryDir: projectMemoryDir(cwd),
60
+ });
61
+ }
62
+
36
63
  export default function memoryExtension(pi: ExtensionAPI) {
37
64
  let dir: string | undefined;
38
65
 
39
66
  pi.on("session_start", (_event, ctx) => {
67
+ const candidate = projectMemoryDir(ctx.cwd);
68
+
69
+ // Claude Code's startup warning: an instruction file over the char limit
70
+ // bloats every turn's context. Warn once, pointing at /memory to trim it.
71
+ // Also warn when several files each fit but together blow the budget —
72
+ // suppressed if a single file already fired, so a lone big file isn't
73
+ // reported twice.
74
+ if (ctx.hasUI) {
75
+ let total = 0;
76
+ let anyPerFile = false;
77
+ for (const entry of memoryEntriesFor(ctx.cwd)) {
78
+ if (entry.kind !== "file" || !entry.exists) continue;
79
+ const content = tryReadFile(entry.path);
80
+ if (content == null) continue;
81
+ total += content.length;
82
+ const warning = claudeMdLimitWarning(entryName(entry), content.length);
83
+ if (warning) {
84
+ anyPerFile = true;
85
+ ctx.ui.notify(warning, "warning");
86
+ }
87
+ }
88
+ if (!anyPerFile) {
89
+ const combined = combinedLimitWarning(total);
90
+ if (combined) ctx.ui.notify(combined, "warning");
91
+ }
92
+ }
93
+
40
94
  // Like Claude Code: one memory dir per git repo (shared by worktrees and
41
95
  // subdirectories); outside a repo, per cwd.
42
- const candidate = projectMemoryDir(ctx.cwd);
43
96
  try {
44
97
  mkdirSync(candidate, { recursive: true });
45
98
  } catch {
@@ -87,4 +140,68 @@ export default function memoryExtension(pi: ExtensionAPI) {
87
140
  }
88
141
  return undefined;
89
142
  });
143
+
144
+ // ---- /memory: Claude Code's Memory picker --------------------------------
145
+ // Pick a memory-related file (or the auto-memory folder) and open it in the
146
+ // external editor, matching CC. Pure panel logic lives in ./panel.ts.
147
+ pi.registerCommand("memory", {
148
+ description: "View or edit CLAUDE.md and memory files",
149
+ handler: async (_args, ctx) => {
150
+ const entries = memoryEntriesFor(ctx.cwd);
151
+ if (!ctx.hasUI) {
152
+ const lines = entries.map(
153
+ (e, i) => `${i + 1}. ${e.title}${e.description ? ` — ${e.description}` : ""} [${e.path}]`,
154
+ );
155
+ ctx.ui.notify(`Memory / CLAUDE.md files:\n${lines.join("\n")}`, "info");
156
+ return;
157
+ }
158
+ const chosen = await openMemoryPanel(ctx, entries);
159
+ if (!chosen) return;
160
+ const result = await openPath(chosen.path, chosen.kind);
161
+ ctx.ui.notify(result.message, result.ok ? "info" : "error");
162
+ if (result.ok && result.hint) ctx.ui.notify(result.hint, "info");
163
+ },
164
+ });
165
+ }
166
+
167
+ /** Show the Memory panel as a focused overlay; resolve to the chosen entry or null. */
168
+ function openMemoryPanel(ctx: ExtensionContext, entries: MemoryEntry[]): Promise<MemoryEntry | null> {
169
+ return ctx.ui.custom<MemoryEntry | null>((tui, theme, _keybindings, done) => {
170
+ const paint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
171
+ const state = initialMemoryState();
172
+ let cache: { width: number; lines: string[] } | undefined;
173
+ const repaint = () => {
174
+ cache = undefined;
175
+ tui.requestRender();
176
+ };
177
+ return {
178
+ render: (width: number) => {
179
+ if (cache?.width === width) return cache.lines;
180
+ const termRows = (tui as { terminal: { rows: number } }).terminal.rows;
181
+ const height = boundedDockHeight(termRows, MEMORY_PANEL_MAX_HEIGHT);
182
+ const lines = renderMemoryPanel({ state, entries, width, height }, paint).map((line) =>
183
+ truncateLine(line, width),
184
+ );
185
+ cache = { width, lines };
186
+ return lines;
187
+ },
188
+ handleInput: (data: string) => {
189
+ const key = decodeMemoryKey(data);
190
+ if (!key) return;
191
+ const effect = applyMemoryKey(state, key, entries);
192
+ if (effect?.kind === "close") {
193
+ done(null);
194
+ return;
195
+ }
196
+ if (effect?.kind === "open") {
197
+ done(effect.entry);
198
+ return;
199
+ }
200
+ repaint();
201
+ },
202
+ invalidate: () => {
203
+ cache = undefined;
204
+ },
205
+ };
206
+ });
90
207
  }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Open a memory file or folder in the user's editor / OS opener, matching Claude
3
+ * Code's `/memory`: a file goes to `$VISUAL`/`$EDITOR` (or the OS default), a
4
+ * folder to the OS file manager. The child is detached and unref'd so the TUI is
5
+ * never blocked (as the MCP OAuth browser open does). Returns the status line and
6
+ * the editor hint to print, matching CC's "Opened <path>" + the $EDITOR nudge.
7
+ *
8
+ * Limitation: a terminal editor (vim/nano) needs the controlling tty, which the
9
+ * pi TUI owns, so it cannot run detached — this suits GUI editors (code, subl)
10
+ * and the OS opener, CC's common desktop case. We can detect a failed *spawn*
11
+ * (an unknown editor command) but not a GUI editor that opens a not-yet-created
12
+ * file (it materialises on save) — so `openPath` reports success once the child
13
+ * spawns. The command choice is pure and unit-tested via `resolveOpen`.
14
+ */
15
+
16
+ import { spawn } from "node:child_process";
17
+
18
+ export interface OpenPlan {
19
+ command: string;
20
+ args: string[];
21
+ }
22
+
23
+ /**
24
+ * Split an `$EDITOR`/`$VISUAL` value into command + flags, honouring single and
25
+ * double quotes so a spaced editor path works when quoted (`"/Apps/My Editor" -w`)
26
+ * — the same contract as a shell, and what tools like git expect. An unquoted
27
+ * space is a token boundary, so a spaced path must be quoted (as elsewhere).
28
+ */
29
+ function tokenizeEditor(editor: string): string[] {
30
+ const tokens: string[] = [];
31
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
32
+ let m: RegExpExecArray | null;
33
+ while ((m = re.exec(editor)) !== null) tokens.push(m[1] ?? m[2] ?? m[3]);
34
+ return tokens;
35
+ }
36
+
37
+ /** Pure: decide the command + args to open `path`. `plat` defaults to the host platform. */
38
+ export function resolveOpen(
39
+ path: string,
40
+ kind: "file" | "folder",
41
+ env: Record<string, string | undefined> = process.env,
42
+ plat: NodeJS.Platform = process.platform,
43
+ ): OpenPlan {
44
+ const editor = kind === "file" ? (env.VISUAL || env.EDITOR)?.trim() : undefined;
45
+ if (editor) {
46
+ const parts = tokenizeEditor(editor);
47
+ return { command: parts[0], args: [...parts.slice(1), path] };
48
+ }
49
+ if (plat === "darwin") return { command: "open", args: [path] };
50
+ if (plat === "win32") return { command: "cmd", args: ["/c", "start", "", path] };
51
+ return { command: "xdg-open", args: [path] };
52
+ }
53
+
54
+ export interface OpenResult {
55
+ message: string;
56
+ hint?: string;
57
+ ok: boolean;
58
+ }
59
+
60
+ /** The editor hint CC prints under an opened file. */
61
+ export const EDITOR_HINT = "To use a different editor, set the $EDITOR or $VISUAL environment variable.";
62
+
63
+ /**
64
+ * Spawn the opener/editor detached, resolving once the child either spawns
65
+ * (success) or fails to spawn (e.g. an unknown `$EDITOR` command — reported as an
66
+ * error instead of a false "Opened"). Both spawn outcomes always fire exactly one
67
+ * of these events, so the promise never hangs.
68
+ */
69
+ export function openPath(path: string, kind: "file" | "folder"): Promise<OpenResult> {
70
+ const { command, args } = resolveOpen(path, kind);
71
+ return new Promise((resolve) => {
72
+ let settled = false;
73
+ const finish = (result: OpenResult) => {
74
+ if (!settled) {
75
+ settled = true;
76
+ resolve(result);
77
+ }
78
+ };
79
+ let child: ReturnType<typeof spawn>;
80
+ try {
81
+ child = spawn(command, args, { stdio: "ignore", detached: true });
82
+ } catch (error) {
83
+ finish({ message: `Could not open ${path}: ${error instanceof Error ? error.message : error}`, ok: false });
84
+ return;
85
+ }
86
+ child.on("error", (error) => finish({ message: `Could not open ${path}: ${error.message}`, ok: false }));
87
+ child.on("spawn", () => {
88
+ child.unref();
89
+ finish({
90
+ message: `Opened ${path}`,
91
+ // CC shows the hint on file opens (even when $EDITOR handled it); folders don't.
92
+ hint: kind === "file" ? EDITOR_HINT : undefined,
93
+ ok: true,
94
+ });
95
+ });
96
+ });
97
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Pure state, key decoding, and rendering for the `/memory` panel — Claude Code's
3
+ * Memory picker: a titled list of the CLAUDE.md-family / AGENTS.md / ONECODE.md
4
+ * files plus "Open auto-memory folder", with an Auto-memory status line and a
5
+ * learn-more link. Enter opens the selected entry (in `$EDITOR`); Esc closes.
6
+ *
7
+ * Kept free of pi imports so the layout and navigation are unit-tested; the
8
+ * extension owns repaint, the overlay, and the actual open.
9
+ */
10
+
11
+ import type { MemoryEntry } from "./entries.ts";
12
+
13
+ export const MEMORY_DOCS_URL = "https://github.com/IsuruMaduranga/one-code";
14
+
15
+ export interface MemoryPanelState {
16
+ cursor: number;
17
+ }
18
+
19
+ export function initialMemoryState(): MemoryPanelState {
20
+ return { cursor: 0 };
21
+ }
22
+
23
+ export type MemoryKey = { kind: "up" | "down" | "enter" | "close" };
24
+
25
+ export function decodeMemoryKey(data: string): MemoryKey | undefined {
26
+ switch (data) {
27
+ case "\x1b[A":
28
+ case "\x1bOA":
29
+ case "\x10": // ctrl+p
30
+ return { kind: "up" };
31
+ case "\x1b[B":
32
+ case "\x1bOB":
33
+ case "\x0e": // ctrl+n
34
+ return { kind: "down" };
35
+ case "\r":
36
+ case "\n":
37
+ return { kind: "enter" };
38
+ case "\x1b":
39
+ case "\x03": // ctrl+c
40
+ return { kind: "close" };
41
+ default:
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ export type MemoryEffect = { kind: "open"; entry: MemoryEntry } | { kind: "close" };
47
+
48
+ export function applyMemoryKey(
49
+ state: MemoryPanelState,
50
+ key: MemoryKey,
51
+ entries: readonly MemoryEntry[],
52
+ ): MemoryEffect | undefined {
53
+ switch (key.kind) {
54
+ case "up":
55
+ state.cursor = Math.max(0, state.cursor - 1);
56
+ return undefined;
57
+ case "down":
58
+ state.cursor = Math.min(entries.length - 1, state.cursor + 1);
59
+ return undefined;
60
+ case "enter": {
61
+ const entry = entries[state.cursor];
62
+ return entry ? { kind: "open", entry } : undefined;
63
+ }
64
+ case "close":
65
+ return { kind: "close" };
66
+ }
67
+ }
68
+
69
+ export type PanelPaint = {
70
+ fg: (color: string, text: string) => string;
71
+ bold: (text: string) => string;
72
+ };
73
+
74
+ /** Column where descriptions begin (after "❯ N. Title"), padded on plain text. */
75
+ const DESC_COL = 34;
76
+
77
+ /**
78
+ * Render the panel to `height` lines. The header (title, status) and footer
79
+ * (learn-more, key hints) are fixed; the entry list scrolls within what's left so
80
+ * the cursor stays visible.
81
+ */
82
+ export function renderMemoryPanel(
83
+ input: { state: MemoryPanelState; entries: readonly MemoryEntry[]; width: number; height: number },
84
+ paint: PanelPaint,
85
+ ): string[] {
86
+ const { state, entries, height } = input;
87
+
88
+ const header = [paint.bold("Memory"), "", ` ${paint.fg("muted", "Auto-memory: on")}`, ""];
89
+ const footer = [
90
+ "",
91
+ paint.fg("muted", `Learn more: ${MEMORY_DOCS_URL}`),
92
+ "",
93
+ paint.fg("muted", "Enter to open · Esc to close"),
94
+ ];
95
+
96
+ const listCapacity = Math.max(1, height - header.length - footer.length);
97
+ const start = scrollStart(state.cursor, entries.length, listCapacity);
98
+ const rows: string[] = [];
99
+ for (let i = start; i < Math.min(entries.length, start + listCapacity); i++) {
100
+ rows.push(renderRow(entries[i], i, i === state.cursor, paint));
101
+ }
102
+
103
+ return [...header, ...rows, ...footer];
104
+ }
105
+
106
+ /** First index to show so `cursor` is within a `capacity`-tall window. */
107
+ function scrollStart(cursor: number, total: number, capacity: number): number {
108
+ if (total <= capacity) return 0;
109
+ const start = Math.min(Math.max(0, cursor - Math.floor(capacity / 2)), total - capacity);
110
+ return Math.max(0, start);
111
+ }
112
+
113
+ function renderRow(entry: MemoryEntry, index: number, selected: boolean, paint: PanelPaint): string {
114
+ const prefix = selected ? "❯ " : " ";
115
+ const left = `${prefix}${index + 1}. ${entry.title}`;
116
+ if (!entry.description) {
117
+ return selected ? paint.fg("accent", left) : left;
118
+ }
119
+ const pad = " ".repeat(Math.max(2, DESC_COL - left.length + 2));
120
+ if (selected) {
121
+ return `${paint.fg("accent", left)}${pad}${paint.fg("accent", entry.description)}`;
122
+ }
123
+ return `${left}${pad}${paint.fg("muted", entry.description)}`;
124
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * model-default extension — remember the interactively chosen model across
3
+ * restarts (Claude Code parity: a /model selection sticks).
4
+ *
5
+ * pi's own selector persists only through its explicit "set as default" action;
6
+ * plain Enter and `/model <name>` switch the session only, so a restart forgets
7
+ * the choice. One Code persists every interactive model change instead:
8
+ * `model_select` fires only for real in-session switches ("set"/"cycle") —
9
+ * never for the startup model, a `--model` launch, or a resumed session's
10
+ * restore — so those paths keep their pi semantics.
11
+ *
12
+ * The value lands in pi's OWN settings.json under getAgentDir()
13
+ * (`defaultProvider`/`defaultModel`), which pi loads natively at the next
14
+ * startup with its normal precedence (an explicit --model still wins). Safe to
15
+ * write externally: pi's settings save re-reads the file under its lock and
16
+ * merges only the fields pi itself modified, so this key survives pi's writes.
17
+ */
18
+
19
+ import { join } from "node:path";
20
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
+ import { readSettingsForWrite, writeSettings } from "../lib/one-code-settings.ts";
22
+
23
+ /** Pure merge: set the default model, preserving every other settings key. */
24
+ export function applyDefaultModel(
25
+ file: Record<string, unknown>,
26
+ provider: string,
27
+ modelId: string,
28
+ ): Record<string, unknown> {
29
+ return { ...file, defaultProvider: provider, defaultModel: modelId };
30
+ }
31
+
32
+ /** Quiet period after the last switch before it is written (ctrl+p cycles fire one event per step). */
33
+ export const PERSIST_DEBOUNCE_MS = 400;
34
+
35
+ export default function modelDefaultExtension(pi: ExtensionAPI) {
36
+ let pending: { provider: string; id: string; notify: (message: string) => void } | undefined;
37
+ let timer: ReturnType<typeof setTimeout> | undefined;
38
+
39
+ const flush = () => {
40
+ if (timer !== undefined) clearTimeout(timer);
41
+ timer = undefined;
42
+ const choice = pending;
43
+ pending = undefined;
44
+ if (!choice) return;
45
+ const path = join(getAgentDir(), "settings.json");
46
+ try {
47
+ writeSettings(path, applyDefaultModel(readSettingsForWrite(path), choice.provider, choice.id));
48
+ } catch (error) {
49
+ // Persisting a preference must not break the model switch itself, but
50
+ // say why it will be forgotten (typically a malformed settings.json).
51
+ choice.notify(`Could not save ${choice.provider}/${choice.id} as the default model: ${(error as Error).message}`);
52
+ }
53
+ };
54
+
55
+ // Debounced: a ctrl+p cycle emits model_select per step, and only the model
56
+ // the user settles on should hit the disk (last one wins).
57
+ pi.on("model_select", (event, ctx) => {
58
+ const model = event.model as { provider?: string; id?: string } | undefined;
59
+ if (!model?.provider || !model.id) return;
60
+ pending = {
61
+ provider: model.provider,
62
+ id: model.id,
63
+ notify: (message) => {
64
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
65
+ },
66
+ };
67
+ if (timer !== undefined) clearTimeout(timer);
68
+ timer = setTimeout(flush, PERSIST_DEBOUNCE_MS);
69
+ timer.unref?.();
70
+ });
71
+
72
+ // A quit inside the quiet period must not lose the choice.
73
+ pi.on("session_shutdown", () => flush());
74
+ }