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,86 @@
1
+ /**
2
+ * Per-skill availability overrides (pure fs, no pi imports).
3
+ *
4
+ * `<plugin root>/skill-overrides.json`: `{"<scope>:<skill name>": SkillState}`.
5
+ * Mirrors Claude Code's `skillOverrides` setting — four states controlling how
6
+ * much of a skill reaches the model and whether the user can still run it:
7
+ *
8
+ * on name + description listed to the model; user-invocable
9
+ * name-only name listed (no description, saving context tokens); user-invocable
10
+ * user-only hidden from the model (won't auto-trigger); user-invocable via /name
11
+ * off hidden from the model AND refused on explicit invocation
12
+ *
13
+ * Absent key = "on" (every discovered skill is fully available by default). The
14
+ * /skills panel cycles these states for project/user skills; plugin skills are
15
+ * "locked" (their on/off is managed via /plugins, stored here under the
16
+ * "plugin" scope as on/off only). Legacy boolean values round-trip as on/off.
17
+ *
18
+ * The boolean-facing `isSkillEnabled`/`setSkillOverride` wrappers keep the
19
+ * /plugins panel and the plugin-skill filter (which only ever enable/disable)
20
+ * working against the same store.
21
+ */
22
+
23
+ import { join } from "node:path";
24
+ import { readJsonFile, writeJsonAtomic } from "./atomic-write.ts";
25
+
26
+ const FILE = "skill-overrides.json";
27
+
28
+ export type SkillScope = "user" | "project" | "plugin";
29
+ export type SkillState = "on" | "name-only" | "user-only" | "off";
30
+
31
+ /** Cycle order for the /skills panel (space/enter advances). */
32
+ export const SKILL_STATES: readonly SkillState[] = ["on", "name-only", "user-only", "off"];
33
+
34
+ export function skillOverrideKey(scope: SkillScope, name: string): string {
35
+ return `${scope}:${name}`;
36
+ }
37
+
38
+ /** Read the store as states, coercing legacy booleans (true → on, false → off). */
39
+ export function readSkillStates(root: string): Record<string, SkillState> {
40
+ const parsed = readJsonFile<Record<string, unknown>>(join(root, FILE));
41
+ if (!parsed || typeof parsed !== "object") return {};
42
+ const map: Record<string, SkillState> = {};
43
+ for (const [key, value] of Object.entries(parsed)) {
44
+ if (typeof value === "string" && (SKILL_STATES as readonly string[]).includes(value)) {
45
+ map[key] = value as SkillState;
46
+ } else if (value === true) {
47
+ map[key] = "on";
48
+ } else if (value === false) {
49
+ map[key] = "off";
50
+ }
51
+ }
52
+ return map;
53
+ }
54
+
55
+ export function setSkillState(root: string, key: string, state: SkillState): void {
56
+ const existing = readJsonFile<Record<string, unknown>>(join(root, FILE)) ?? {};
57
+ writeJsonAtomic(join(root, FILE), { ...existing, [key]: state });
58
+ }
59
+
60
+ export function skillStateFor(states: Record<string, SkillState>, key: string): SkillState {
61
+ return states[key] ?? "on";
62
+ }
63
+
64
+ /** Advance to the next state in the cycle (wraps). */
65
+ export function nextSkillState(state: SkillState): SkillState {
66
+ return SKILL_STATES[(SKILL_STATES.indexOf(state) + 1) % SKILL_STATES.length];
67
+ }
68
+
69
+ /** How much of the skill the model's listing should carry for a given state. */
70
+ export function skillListingVisibility(state: SkillState): "full" | "name" | "hidden" {
71
+ return state === "on" ? "full" : state === "name-only" ? "name" : "hidden";
72
+ }
73
+
74
+ // --- Boolean-facing compatibility (plugins panel + plugin-skill filter) ---
75
+
76
+ /** Enabled = anything but "off"; the plugin filter and Installed tab use this. */
77
+ export function isSkillEnabled(states: Record<string, SkillState>, key: string): boolean {
78
+ return skillStateFor(states, key) !== "off";
79
+ }
80
+
81
+ export function setSkillOverride(root: string, key: string, enabled: boolean): void {
82
+ setSkillState(root, key, enabled ? "on" : "off");
83
+ }
84
+
85
+
86
+
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Skill discovery for the pre-first-turn /skills panel and the bare `/<skill>`
3
+ * command registration (pure fs). (The /plugins Installed tab builds its rows
4
+ * from discoverPlugins directly and no longer calls this.)
5
+ *
6
+ * Scans the SAME directories claude-compat feeds to pi, in pi's order —
7
+ * `~/.claude/skills`, `~/.agents/skills`, `<cwd>/.claude/skills`,
8
+ * `<cwd>/.agents/skills`, `<agentDir>/skills`, then the bundled catalog last
9
+ * (so it loses a name collision, as it does in pi) — and merges the plugin
10
+ * skills the caller resolved. Rows carry the SKILL.md path (for the ~token
11
+ * size estimate) and the scope used by skill-overrides keys. Before the first
12
+ * turn this scan is the only skill list One Code has (pi resolves skills per
13
+ * turn), so a root missing here is a skill with no `/name` command at startup.
14
+ */
15
+
16
+ import { existsSync, readdirSync, statSync } from "node:fs";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import type { PluginSkill } from "./plugins.ts";
20
+ import type { SkillScope } from "./skill-overrides.ts";
21
+ import { claudeUserDir } from "./paths.ts";
22
+
23
+ /** The skill catalog shipped in this package: `<package>/skills` (Claude Code's self-contained built-in skills). */
24
+ export const BUNDLED_SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
25
+
26
+ export interface ScannedSkill {
27
+ name: string;
28
+ path: string;
29
+ scope: SkillScope;
30
+ }
31
+
32
+ /**
33
+ * The one scope-classification rule for a skill path — user skills live under
34
+ * `~/.claude/skills` AND `<agentDir>/skills`; everything else is project
35
+ * scope. Both the scanner and the skill extension classify through this, so a
36
+ * skill never shows different scopes in /plugins vs the skill tool.
37
+ */
38
+ export function scopeForPath(path: string, home: string, agentDir: string): SkillScope {
39
+ if (path.startsWith(join(claudeUserDir(home), "skills")) || path.startsWith(join(agentDir, "skills"))) return "user";
40
+ return "project";
41
+ }
42
+
43
+ function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>): void {
44
+ if (!existsSync(dir)) return;
45
+ let entries: string[];
46
+ try {
47
+ entries = readdirSync(dir);
48
+ } catch {
49
+ return;
50
+ }
51
+ for (const entry of entries) {
52
+ const skillFile = join(dir, entry, "SKILL.md");
53
+ // existsSync follows symlinked skill directories, which isDirectory() would miss.
54
+ if (existsSync(skillFile) && !into.has(`${scope}:${entry}`)) {
55
+ into.set(`${scope}:${entry}`, { name: entry, path: skillFile, scope });
56
+ }
57
+ }
58
+ }
59
+
60
+ export function scanSkills(
61
+ cwd: string,
62
+ home: string,
63
+ agentDir: string,
64
+ pluginSkills: PluginSkill[],
65
+ bundledSkillsDir?: string,
66
+ ): ScannedSkill[] {
67
+ const skills = new Map<string, ScannedSkill>();
68
+ scanDir(join(claudeUserDir(home), "skills"), "user", skills);
69
+ scanDir(join(home, ".agents", "skills"), "user", skills);
70
+ scanDir(join(cwd, ".claude", "skills"), "project", skills);
71
+ scanDir(join(cwd, ".agents", "skills"), "project", skills);
72
+ scanDir(join(agentDir, "skills"), "user", skills);
73
+ if (bundledSkillsDir) scanDir(bundledSkillsDir, scopeForPath(bundledSkillsDir, home, agentDir), skills);
74
+ for (const skill of pluginSkills) {
75
+ skills.set(`plugin:${skill.name}`, { name: skill.name, path: skill.path, scope: "plugin" });
76
+ }
77
+ return [...skills.values()].sort((a, b) => a.name.localeCompare(b.name));
78
+ }
79
+
80
+ /**
81
+ * Names of the `.claude/commands` prompt templates (`<name>.md`) pi will expose
82
+ * as `/<name>` — the same dirs claude-compat feeds pi (`~/.claude/commands`,
83
+ * `<cwd>/.claude/commands`) plus pi's own `<agentDir>/prompts`. pi resolves
84
+ * templates per turn, after `session_start`, so a bare skill command registered
85
+ * then would silently shadow a same-named template for the whole session;
86
+ * this pre-scan lets the registration skip those names.
87
+ */
88
+ export function promptTemplateNames(cwd: string, home: string, agentDir: string): string[] {
89
+ const names = new Set<string>();
90
+ for (const dir of [join(claudeUserDir(home), "commands"), join(cwd, ".claude", "commands"), join(agentDir, "prompts")]) {
91
+ if (!existsSync(dir)) continue;
92
+ let entries: string[];
93
+ try {
94
+ entries = readdirSync(dir);
95
+ } catch {
96
+ continue;
97
+ }
98
+ for (const entry of entries) if (entry.endsWith(".md")) names.add(entry.slice(0, -3));
99
+ }
100
+ return [...names];
101
+ }
102
+
103
+ /** Rough token estimate from the SKILL.md byte size (~4 bytes/token). */
104
+ export function estimateSkillTokens(path: string): number {
105
+ try {
106
+ return Math.max(1, Math.round(statSync(path).size / 4));
107
+ } catch {
108
+ return 0;
109
+ }
110
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Terminal-column width for text, matching pi-tui's own measurement.
3
+ *
4
+ * pi-tui measures a rendered line in terminal *columns* — a CJK ideograph or
5
+ * an emoji is two columns, a combining mark zero — and in regular TUI mode it
6
+ * *crashes the whole app* on a line wider than the terminal ("Rendered line
7
+ * exceeds terminal width"). Our width helpers historically counted code points
8
+ * (`[...text].length`) or UTF-16 units (`line.length`), so a line of N wide
9
+ * glyphs at N ≤ width was "fitted" at 2N columns and killed pi (TUI-REVIEW H1).
10
+ *
11
+ * This module reproduces pi-tui's `visibleWidth`/`graphemeWidth`
12
+ * (`@earendil-works/pi-tui` `dist/utils.js`) so every helper on top of it
13
+ * truncates by the same measure pi-tui validates against. It stays a pure,
14
+ * zero-dependency module: the East Asian wide/fullwidth ranges below are the
15
+ * `wideRanges`/`fullwidthRanges` tables from `get-east-asian-width` (MIT,
16
+ * © Sindre Sorhus — pi-tui's own width dependency), embedded verbatim so the
17
+ * 2-vs-1 decision is byte-identical to pi's without importing pi or adding a
18
+ * package. Grapheme segmentation and the mark/emoji classification use V8's
19
+ * built-in `Intl.Segmenter` and Unicode property regexes.
20
+ */
21
+
22
+ // --- East Asian wide/fullwidth lookup (get-east-asian-width v1.6.0, Unicode 16) ---
23
+ // Flat, sorted [start, end] inclusive pairs. `eastAsianWidth(cp)` returns 2 when
24
+ // cp is fullwidth or wide, else 1 — the only distinction pi-tui's grapheme
25
+ // width needs (ambiguous is treated as narrow, `ambiguousAsWide` off).
26
+ const FULLWIDTH_MIN = 12288;
27
+ const FULLWIDTH_MAX = 65510;
28
+ // prettier-ignore
29
+ const FULLWIDTH_RANGES = [12288, 12288, 65281, 65376, 65504, 65510];
30
+ const WIDE_MIN = 4352;
31
+ const WIDE_MAX = 262141;
32
+ // prettier-ignore
33
+ const WIDE_RANGES = [
34
+ 4352, 4447, 8986, 8987, 9001, 9002, 9193, 9196, 9200, 9200, 9203, 9203, 9725, 9726, 9748, 9749, 9776, 9783,
35
+ 9800, 9811, 9855, 9855, 9866, 9871, 9875, 9875, 9889, 9889, 9898, 9899, 9917, 9918, 9924, 9925, 9934, 9934,
36
+ 9940, 9940, 9962, 9962, 9970, 9971, 9973, 9973, 9978, 9978, 9981, 9981, 9989, 9989, 9994, 9995, 10024,
37
+ 10024, 10060, 10060, 10062, 10062, 10067, 10069, 10071, 10071, 10133, 10135, 10160, 10160, 10175, 10175,
38
+ 11035, 11036, 11088, 11088, 11093, 11093, 11904, 11929, 11931, 12019, 12032, 12245, 12272, 12287, 12289,
39
+ 12350, 12353, 12438, 12441, 12543, 12549, 12591, 12593, 12686, 12688, 12773, 12783, 12830, 12832, 12871,
40
+ 12880, 42124, 42128, 42182, 43360, 43388, 44032, 55203, 63744, 64255, 65040, 65049, 65072, 65106, 65108,
41
+ 65126, 65128, 65131, 94176, 94180, 94192, 94198, 94208, 101589, 101631, 101662, 101760, 101874, 110576,
42
+ 110579, 110581, 110587, 110589, 110590, 110592, 110882, 110898, 110898, 110928, 110930, 110933, 110933,
43
+ 110948, 110951, 110960, 111355, 119552, 119638, 119648, 119670, 126980, 126980, 127183, 127183, 127374,
44
+ 127374, 127377, 127386, 127488, 127490, 127504, 127547, 127552, 127560, 127568, 127569, 127584, 127589,
45
+ 127744, 127776, 127789, 127797, 127799, 127868, 127870, 127891, 127904, 127946, 127951, 127955, 127968,
46
+ 127984, 127988, 127988, 127992, 128062, 128064, 128064, 128066, 128252, 128255, 128317, 128331, 128334,
47
+ 128336, 128359, 128378, 128378, 128405, 128406, 128420, 128420, 128507, 128591, 128640, 128709, 128716,
48
+ 128716, 128720, 128722, 128725, 128728, 128732, 128735, 128747, 128748, 128756, 128764, 128992, 129003,
49
+ 129008, 129008, 129292, 129338, 129340, 129349, 129351, 129535, 129648, 129660, 129664, 129674, 129678,
50
+ 129734, 129736, 129736, 129741, 129756, 129759, 129770, 129775, 129784, 131072, 196605, 196608, 262141,
51
+ ];
52
+
53
+ /** Binary search over a flat, sorted array of inclusive [start, end] pairs. */
54
+ function inRange(ranges: number[], codePoint: number): boolean {
55
+ let low = 0;
56
+ let high = ranges.length / 2 - 1;
57
+ while (low <= high) {
58
+ const mid = (low + high) >> 1;
59
+ const i = mid * 2;
60
+ if (codePoint < ranges[i]) high = mid - 1;
61
+ else if (codePoint > ranges[i + 1]) low = mid + 1;
62
+ else return true;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ /** 2 for East Asian fullwidth/wide code points, 1 otherwise. */
68
+ function eastAsianWidth(codePoint: number): number {
69
+ if (codePoint >= FULLWIDTH_MIN && codePoint <= FULLWIDTH_MAX && inRange(FULLWIDTH_RANGES, codePoint)) return 2;
70
+ if (codePoint >= WIDE_MIN && codePoint <= WIDE_MAX && inRange(WIDE_RANGES, codePoint)) return 2;
71
+ return 1;
72
+ }
73
+
74
+ // --- Grapheme classification (mirrors pi-tui dist/utils.js) ---
75
+ // `Intl.Segmenter` is a V8 built-in; the ES2023 lib does not always type it.
76
+ const Segmenter: new (locale?: string, options?: { granularity: string }) => {
77
+ segment(input: string): Iterable<{ segment: string }>;
78
+ } = (Intl as unknown as { Segmenter: any }).Segmenter;
79
+ const graphemeSegmenter = new Segmenter(undefined, { granularity: "grapheme" });
80
+
81
+ // Built via `new RegExp(…, "v")` so the `v` (Unicode sets) flag — needed for
82
+ // `\p{RGI_Emoji}` and the set-subtraction below, and supported at runtime on
83
+ // Node 22+/26 — is not rejected by the ES2023 type target's static flag check.
84
+ const zeroWidthRegex = new RegExp(String.raw`^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Mark}|\p{Surrogate})+$`, "v");
85
+ const leadingNonPrintingRegex = new RegExp(String.raw`^[\p{Default_Ignorable_Code_Point}\p{Control}\p{Format}\p{Mark}\p{Surrogate}]+`, "v");
86
+ const nonPrintingCharRegex = new RegExp(String.raw`^(?:\p{Default_Ignorable_Code_Point}|\p{Control}|\p{Format}|\p{Mark}|\p{Surrogate})$`, "v");
87
+ const markCharRegex = new RegExp(String.raw`^\p{Mark}$`, "v");
88
+ // Marks that terminals allocate cells for when attached to a base character.
89
+ const terminalSpacingMarkRegex = new RegExp(
90
+ String.raw`^(?:[\p{Spacing_Mark}--[᜴〮〯]]|[ٟཿါာေဳ-ဵး်-ှ])+$`,
91
+ "v",
92
+ );
93
+ const rgiEmojiRegex = new RegExp(String.raw`^\p{RGI_Emoji}$`, "v");
94
+
95
+ /** Fast heuristic to skip the expensive RGI_Emoji test for clearly-non-emoji clusters. */
96
+ function couldBeEmoji(segment: string): boolean {
97
+ const cp = segment.codePointAt(0) ?? 0;
98
+ return (
99
+ (cp >= 0x1f000 && cp <= 0x1fbff) ||
100
+ (cp >= 0x2300 && cp <= 0x23ff) ||
101
+ (cp >= 0x2600 && cp <= 0x27bf) ||
102
+ (cp >= 0x2b50 && cp <= 0x2b55) ||
103
+ segment.includes("️") ||
104
+ segment.length > 2
105
+ );
106
+ }
107
+
108
+ /** Terminal columns occupied by one grapheme cluster. Mirrors pi-tui's graphemeWidth. */
109
+ export function graphemeWidth(segment: string): number {
110
+ if (segment === "\t") return 3;
111
+ if (terminalSpacingMarkRegex.test(segment)) return [...segment].length;
112
+ if (zeroWidthRegex.test(segment)) return 0;
113
+ if (couldBeEmoji(segment) && rgiEmojiRegex.test(segment)) return 2;
114
+ const base = segment.replace(leadingNonPrintingRegex, "");
115
+ const cp = base.codePointAt(0);
116
+ if (cp === undefined) return 0;
117
+ // Regional-indicator symbols render as full-width emoji even in isolation.
118
+ if (cp >= 0x1f1e6 && cp <= 0x1f1ff) return 2;
119
+ let width = eastAsianWidth(cp);
120
+ let followsMark = false;
121
+ for (const char of [...base].slice(1)) {
122
+ if (terminalSpacingMarkRegex.test(char)) {
123
+ width += 1;
124
+ followsMark = false;
125
+ } else if (markCharRegex.test(char)) {
126
+ followsMark = true;
127
+ } else if (!nonPrintingCharRegex.test(char)) {
128
+ const c = char.codePointAt(0)!;
129
+ if (followsMark || (c >= 0xff00 && c <= 0xffef)) width += eastAsianWidth(c);
130
+ followsMark = false;
131
+ }
132
+ }
133
+ return width;
134
+ }
135
+
136
+ // --- ANSI / OSC / APC escape handling ---
137
+ // A single terminal control sequence starting at index 0 of `s`, or null.
138
+ // Covers CSI (styling/cursor), OSC (hyperlinks, prompt markers, terminated by
139
+ // BEL or ST) and APC (terminated by ST) — the same families pi-tui strips.
140
+ function escapeAt(s: string, i: number): number {
141
+ if (s.charCodeAt(i) !== 0x1b) return 0;
142
+ const next = s[i + 1];
143
+ if (next === "[") {
144
+ // CSI: ESC [ ... final byte 0x40–0x7E
145
+ let j = i + 2;
146
+ while (j < s.length) {
147
+ const c = s.charCodeAt(j);
148
+ j++;
149
+ if (c >= 0x40 && c <= 0x7e) return j - i;
150
+ }
151
+ return s.length - i;
152
+ }
153
+ if (next === "]" || next === "_" || next === "P" || next === "^") {
154
+ // OSC / APC / DCS / PM: ESC <intro> ... terminated by BEL or ST (ESC \)
155
+ let j = i + 2;
156
+ while (j < s.length) {
157
+ if (s.charCodeAt(j) === 0x07) return j + 1 - i; // BEL
158
+ if (s.charCodeAt(j) === 0x1b && s[j + 1] === "\\") return j + 2 - i; // ST
159
+ j++;
160
+ }
161
+ return s.length - i;
162
+ }
163
+ // Other two-byte escapes (ESC c, ESC =, …)
164
+ return next ? 2 : 1;
165
+ }
166
+
167
+ const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
168
+
169
+ /**
170
+ * Visible width of a string in terminal columns: ANSI/OSC/APC escapes count
171
+ * zero, tabs count three, wide glyphs and emoji count two, combining marks
172
+ * zero. This is the measure pi-tui validates a rendered line against.
173
+ */
174
+ export function visibleWidth(text: string): number {
175
+ if (text.length === 0) return 0;
176
+ if (PRINTABLE_ASCII.test(text)) return text.length;
177
+ let clean = "";
178
+ for (let i = 0; i < text.length; ) {
179
+ const esc = escapeAt(text, i);
180
+ if (esc) {
181
+ i += esc;
182
+ continue;
183
+ }
184
+ clean += text[i];
185
+ i++;
186
+ }
187
+ // A painted line is usually plain-ASCII text wrapped in colour escapes; once
188
+ // the escapes are gone the fast path applies again, skipping the segmenter and
189
+ // per-grapheme regexes on the common case (every coloured menu/status line).
190
+ if (PRINTABLE_ASCII.test(clean)) return clean.length;
191
+ let width = 0;
192
+ for (const { segment } of graphemeSegmenter.segment(clean)) width += graphemeWidth(segment);
193
+ return width;
194
+ }
195
+
196
+ /**
197
+ * Longest prefix of PLAIN (escape-free) text whose visible width is ≤ `maxCols`,
198
+ * with the width actually taken. Splits on grapheme boundaries, so a wide glyph
199
+ * is kept whole or dropped whole — never half-included past the limit. Plain text
200
+ * is the escape-free case of `fitPainted`, so this delegates rather than keeping
201
+ * a second copy of the grapheme walk.
202
+ */
203
+ export function sliceColumns(text: string, maxCols: number): { text: string; width: number } {
204
+ return fitPainted(text, maxCols);
205
+ }
206
+
207
+ /**
208
+ * Hard-wrap text into chunks each ≤ `columns` visible columns, on grapheme
209
+ * boundaries. A single glyph wider than the whole budget occupies its own chunk
210
+ * (nothing can be dropped without losing content). The shared engine behind
211
+ * `wrapPlainText` and the subagent panel's prose wrap.
212
+ */
213
+ export function hardWrapColumns(text: string, columns: number): string[] {
214
+ const cols = Math.max(1, columns);
215
+ if (text.length === 0) return [""];
216
+ const out: string[] = [];
217
+ let rest = text;
218
+ // Each step consumes a fitPainted prefix and advances; `fitPainted` reports
219
+ // whether it took the whole remainder, so no separate full-width rescan is
220
+ // needed (that rescan made wrapping O(n²) — TUI-REVIEW /simplify).
221
+ for (;;) {
222
+ const { text: head } = fitPainted(rest, cols);
223
+ const chunk = head || [...rest][0] || rest; // guarantee progress on an over-wide glyph
224
+ if (chunk.length >= rest.length) {
225
+ out.push(rest);
226
+ return out;
227
+ }
228
+ out.push(chunk);
229
+ rest = rest.slice(chunk.length);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Longest prefix of a PAINTED line (interspersed ANSI/OSC escapes) whose visible
235
+ * width is ≤ `maxCols`. Escapes are kept verbatim and cost zero columns; visible
236
+ * runs are cut on grapheme boundaries. Returns the prefix and its visible width;
237
+ * the caller supplies its own reset/ellipsis.
238
+ */
239
+ export function fitPainted(line: string, maxCols: number): { text: string; width: number } {
240
+ if (maxCols <= 0) return { text: "", width: 0 };
241
+ if (PRINTABLE_ASCII.test(line)) {
242
+ const clipped = line.slice(0, maxCols);
243
+ return { text: clipped, width: clipped.length };
244
+ }
245
+ let out = "";
246
+ let width = 0;
247
+ let i = 0;
248
+ while (i < line.length) {
249
+ const esc = escapeAt(line, i);
250
+ if (esc) {
251
+ out += line.slice(i, i + esc);
252
+ i += esc;
253
+ continue;
254
+ }
255
+ // Consume the visible run up to the next escape, segment it into graphemes.
256
+ let end = i;
257
+ while (end < line.length && line.charCodeAt(end) !== 0x1b) end++;
258
+ for (const { segment } of graphemeSegmenter.segment(line.slice(i, end))) {
259
+ const w = graphemeWidth(segment);
260
+ if (width + w > maxCols) return { text: out, width };
261
+ out += segment;
262
+ width += w;
263
+ }
264
+ i = end;
265
+ }
266
+ return { text: out, width };
267
+ }