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.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -217
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- 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
|
+
}
|