one-code-extension 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +202 -0
  3. package/agents/explore.md +24 -0
  4. package/agents/general-purpose.md +21 -0
  5. package/agents/plan.md +22 -0
  6. package/docs/decisions/auto-mode.md +526 -0
  7. package/docs/decisions/branding.md +127 -0
  8. package/docs/decisions/code-review-remediation.md +106 -0
  9. package/docs/decisions/compaction.md +140 -0
  10. package/docs/decisions/distribution.md +133 -0
  11. package/docs/decisions/hooks.md +64 -0
  12. package/docs/decisions/lsp.md +58 -0
  13. package/docs/decisions/mcp.md +104 -0
  14. package/docs/decisions/memory-state.md +170 -0
  15. package/docs/decisions/model-policy.md +126 -0
  16. package/docs/decisions/modes.md +178 -0
  17. package/docs/decisions/skills-plugins.md +56 -0
  18. package/docs/decisions/subagents-workflows.md +225 -0
  19. package/docs/decisions/system-prompt.md +98 -0
  20. package/docs/decisions/tools.md +407 -0
  21. package/docs/decisions/tui.md +202 -0
  22. package/docs/decisions.md +148 -0
  23. package/extensions/ask-user/index.ts +124 -0
  24. package/extensions/ask-user/questions.ts +60 -0
  25. package/extensions/auto-mode/actions.ts +78 -0
  26. package/extensions/auto-mode/classifier.ts +388 -0
  27. package/extensions/auto-mode/config.ts +237 -0
  28. package/extensions/auto-mode/decision-log.ts +60 -0
  29. package/extensions/auto-mode/defaults.ts +65 -0
  30. package/extensions/auto-mode/instructions.ts +74 -0
  31. package/extensions/auto-mode/model-picker.ts +225 -0
  32. package/extensions/auto-mode/model-select.ts +178 -0
  33. package/extensions/auto-mode/paths.ts +109 -0
  34. package/extensions/auto-mode/pause.ts +96 -0
  35. package/extensions/auto-mode/prompt.ts +307 -0
  36. package/extensions/auto-mode/rules.ts +96 -0
  37. package/extensions/auto-mode/safety-floor.ts +100 -0
  38. package/extensions/auto-mode/sensitive.ts +137 -0
  39. package/extensions/auto-mode/shell-analysis.ts +702 -0
  40. package/extensions/background/index.ts +405 -0
  41. package/extensions/background/registry.ts +81 -0
  42. package/extensions/background/wakeup.ts +41 -0
  43. package/extensions/bash/background.ts +136 -0
  44. package/extensions/bash/index.ts +149 -0
  45. package/extensions/branding/assistant-marker.ts +53 -0
  46. package/extensions/branding/index.ts +366 -0
  47. package/extensions/branding/prompt-editor.ts +42 -0
  48. package/extensions/branding/prompt-marker.ts +29 -0
  49. package/extensions/branding/startup.ts +152 -0
  50. package/extensions/claude-compat/index.ts +50 -0
  51. package/extensions/claude-context/index.ts +79 -0
  52. package/extensions/compaction/index.ts +231 -0
  53. package/extensions/compaction/prompt.ts +191 -0
  54. package/extensions/context-management/index.ts +147 -0
  55. package/extensions/effort/index.ts +216 -0
  56. package/extensions/effort/slider.ts +253 -0
  57. package/extensions/exit/index.ts +28 -0
  58. package/extensions/file-tracker/index.ts +103 -0
  59. package/extensions/file-tracker/tracker.ts +145 -0
  60. package/extensions/hooks/debug.ts +53 -0
  61. package/extensions/hooks/executor.ts +132 -0
  62. package/extensions/hooks/index.ts +310 -0
  63. package/extensions/hooks/matcher.ts +72 -0
  64. package/extensions/hooks/plugin-hooks.ts +79 -0
  65. package/extensions/hooks/protocol.ts +151 -0
  66. package/extensions/hooks/settings.ts +163 -0
  67. package/extensions/hooks/trust.ts +151 -0
  68. package/extensions/lib/branch-restore.ts +35 -0
  69. package/extensions/lib/claude-context.ts +148 -0
  70. package/extensions/lib/deferred.ts +119 -0
  71. package/extensions/lib/git.ts +14 -0
  72. package/extensions/lib/memory.ts +219 -0
  73. package/extensions/lib/model-policy.ts +436 -0
  74. package/extensions/lib/model-tier.ts +99 -0
  75. package/extensions/lib/notifications.ts +18 -0
  76. package/extensions/lib/paths.ts +26 -0
  77. package/extensions/lib/per-cwd.ts +21 -0
  78. package/extensions/lib/persisted-output.ts +70 -0
  79. package/extensions/lib/pi-version.ts +54 -0
  80. package/extensions/lib/plugins.ts +227 -0
  81. package/extensions/lib/reminders.ts +212 -0
  82. package/extensions/lib/scratchpad.ts +68 -0
  83. package/extensions/lib/tui-render.ts +522 -0
  84. package/extensions/lsp/client.ts +278 -0
  85. package/extensions/lsp/format.ts +46 -0
  86. package/extensions/lsp/index.ts +193 -0
  87. package/extensions/lsp/protocol.ts +64 -0
  88. package/extensions/lsp/servers.ts +79 -0
  89. package/extensions/mcp/client.ts +205 -0
  90. package/extensions/mcp/config.ts +199 -0
  91. package/extensions/mcp/index.ts +373 -0
  92. package/extensions/mcp/schema.ts +161 -0
  93. package/extensions/memory/index.ts +90 -0
  94. package/extensions/notebook/index.ts +77 -0
  95. package/extensions/notebook/notebook.ts +108 -0
  96. package/extensions/permissions/index.ts +876 -0
  97. package/extensions/permissions/matcher.ts +455 -0
  98. package/extensions/permissions/modes.ts +107 -0
  99. package/extensions/permissions/protected-paths.ts +125 -0
  100. package/extensions/permissions/settings.ts +98 -0
  101. package/extensions/plan-mode/index.ts +209 -0
  102. package/extensions/plan-mode/reminder.ts +33 -0
  103. package/extensions/plan-mode/slug.ts +28 -0
  104. package/extensions/plan-mode/viewer.ts +124 -0
  105. package/extensions/plugins/index.ts +115 -0
  106. package/extensions/plugins/template.ts +42 -0
  107. package/extensions/search-tools/index.ts +0 -0
  108. package/extensions/search-tools/tools.ts +24 -0
  109. package/extensions/skill/index.ts +170 -0
  110. package/extensions/subagents/agents.ts +126 -0
  111. package/extensions/subagents/child.ts +414 -0
  112. package/extensions/subagents/default-model.ts +122 -0
  113. package/extensions/subagents/index.ts +1119 -0
  114. package/extensions/subagents/model-select.ts +346 -0
  115. package/extensions/subagents/rpc-turns.ts +123 -0
  116. package/extensions/subagents/runs.ts +93 -0
  117. package/extensions/subagents/usage.ts +53 -0
  118. package/extensions/subagents/worktree.ts +69 -0
  119. package/extensions/system-prompt/environment.ts +35 -0
  120. package/extensions/system-prompt/index.ts +54 -0
  121. package/extensions/system-prompt/template.ts +103 -0
  122. package/extensions/system-prompt/tiers/common.ts +63 -0
  123. package/extensions/system-prompt/tiers/frontier.ts +24 -0
  124. package/extensions/system-prompt/tiers/low.ts +71 -0
  125. package/extensions/system-prompt/tiers/mid.ts +56 -0
  126. package/extensions/system-reminder/index.ts +37 -0
  127. package/extensions/tasks/index.ts +140 -0
  128. package/extensions/tasks/store.ts +174 -0
  129. package/extensions/todo/index.ts +133 -0
  130. package/extensions/tool-search/index.ts +168 -0
  131. package/extensions/tool-style/index.ts +84 -0
  132. package/extensions/web/index.ts +43 -0
  133. package/extensions/web-fetch/extract.ts +139 -0
  134. package/extensions/web-fetch/index.ts +291 -0
  135. package/extensions/web-fetch/summarize.ts +78 -0
  136. package/extensions/workflow/agent-session.ts +368 -0
  137. package/extensions/workflow/globals.ts +251 -0
  138. package/extensions/workflow/index.ts +364 -0
  139. package/extensions/workflow/journal.ts +75 -0
  140. package/extensions/workflow/permission-gate.ts +71 -0
  141. package/extensions/workflow/run-manager.ts +306 -0
  142. package/extensions/workflow/saved-workflows.ts +45 -0
  143. package/extensions/workflow/script-source.ts +201 -0
  144. package/extensions/workflow/types.ts +99 -0
  145. package/extensions/workflow/vm-runtime.ts +31 -0
  146. package/extensions/workflow/widget.ts +56 -0
  147. package/extensions/worktree/index.ts +266 -0
  148. package/extensions/worktree/rewrite.ts +59 -0
  149. package/package.json +101 -0
  150. package/themes/one-code-light.json +76 -0
  151. package/themes/one-code.json +76 -0
  152. package/types/app.d.ts +5 -0
  153. package/types/community-packages.d.ts +12 -0
@@ -0,0 +1,170 @@
1
+ /**
2
+ * skill extension — Claude Code's Skill tool.
3
+ *
4
+ * pi's own mechanism lists skills in the system prompt and expects the model to
5
+ * `read` the SKILL.md path. Claude Code instead exposes a `skill` tool that
6
+ * returns the skill's instructions as a tool result. This adds that tool, so a
7
+ * skill can be invoked by name — including plugin skills as `<plugin>:<skill>`.
8
+ *
9
+ * Skills discovered by pi (which includes `~/.claude/skills` and
10
+ * `.claude/skills` thanks to the claude-compat extension) are read from
11
+ * `before_agent_start`'s systemPromptOptions rather than rediscovered here.
12
+ */
13
+
14
+ import { readFileSync } from "node:fs";
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
17
+ import { Type } from "typebox";
18
+ import os from "node:os";
19
+ import { join } from "node:path";
20
+ import { discoverPlugins } from "../lib/plugins.ts";
21
+ import { CONTEXT_ORDER, REMINDER_CHANNEL } from "../lib/reminders.ts";
22
+ import { ccToolRenderers } from "../lib/tui-render.ts";
23
+
24
+ interface IndexedSkill {
25
+ name: string;
26
+ description?: string;
27
+ path: string;
28
+ source: "project" | "plugin";
29
+ }
30
+
31
+ export default function skillExtension(pi: ExtensionAPI) {
32
+ /** pi resolves skills per turn; cache the latest list for the tool to use. */
33
+ let piSkills: IndexedSkill[] = [];
34
+
35
+ pi.on("before_agent_start", (event) => {
36
+ const skills = event.systemPromptOptions.skills ?? [];
37
+ piSkills = skills.map((skill) => {
38
+ const record = skill as unknown as { name: string; description?: string; path?: string; filePath?: string };
39
+ return {
40
+ name: record.name,
41
+ description: record.description,
42
+ path: record.path ?? record.filePath ?? "",
43
+ source: "project" as const,
44
+ };
45
+ });
46
+ // Claude Code lists skills as a <system-reminder> on the first user message
47
+ // (its block 3), framed for the Skill tool — not in the system prompt.
48
+ const listing = describe();
49
+ if (listing !== "(no skills available)") {
50
+ pi.events.emit(REMINDER_CHANNEL, {
51
+ text: `The following skills are available for use with the Skill tool:\n\n${listing}`,
52
+ scope: "every-turn",
53
+ key: "skills",
54
+ placement: "first-prepend",
55
+ order: CONTEXT_ORDER.skills,
56
+ });
57
+ }
58
+ });
59
+
60
+ const index = (): IndexedSkill[] => [
61
+ ...piSkills.filter((skill) => skill.path),
62
+ ...discoverPlugins(join(os.homedir(), ".claude")).skills.map((skill) => ({
63
+ name: skill.name,
64
+ path: skill.path,
65
+ source: "plugin" as const,
66
+ })),
67
+ ];
68
+
69
+ const describe = () => {
70
+ const all = index();
71
+ if (all.length === 0) return "(no skills available)";
72
+ return all
73
+ .map((skill) => `- ${skill.name}${skill.description ? `: ${skill.description.split("\n")[0]}` : ""}`)
74
+ .join("\n");
75
+ };
76
+
77
+ pi.registerTool({
78
+ name: "skill",
79
+ label: "Skill",
80
+ ...ccToolRenderers<{ skill?: string; args?: string }>("Skill", {
81
+ title: (a) => (a ? [a.skill, a.args].filter(Boolean).join(" ") : undefined),
82
+ // The full instruction text goes to the model; the transcript needs one line.
83
+ result: (_r, a, isError) => (isError ? undefined : a?.skill ? `Loaded ${a.skill}` : undefined),
84
+ }),
85
+ description:
86
+ "Invoke a skill: a packaged set of instructions for a particular kind of task. Call this when the task at hand matches an available skill, or when the user asks for one by name (including `/<name>`). Returns the skill's instructions to follow. Plugin skills are named `<plugin>:<skill>`. Use `list` to see what is available.",
87
+ promptSnippet: "Load packaged instructions for a task (see the skills listing)",
88
+ parameters: Type.Object({
89
+ skill: Type.Optional(Type.String({ description: "Exact skill name, no leading slash" })),
90
+ args: Type.Optional(Type.String({ description: "Arguments to pass through to the skill" })),
91
+ list: Type.Optional(Type.Boolean({ description: "List available skills instead of invoking one" })),
92
+ }),
93
+ async execute(_toolCallId, params) {
94
+ // `list`, or a bare call with no invoke signal, browses the catalog. A
95
+ // call that passed `args` but no `skill` is an invocation that forgot to
96
+ // name the skill — fail loudly rather than silently returning the
97
+ // catalog, which a weak model reads as a non-sequitur (same archetype as
98
+ // the subagent tool; see docs/decisions/subagents-workflows.md).
99
+ if (params.list || (!params.skill && params.args == null)) {
100
+ return {
101
+ content: [{ type: "text", text: `Available skills:\n${describe()}` }],
102
+ details: { skills: index().map((s) => s.name) } as Record<string, unknown>,
103
+ };
104
+ }
105
+ if (!params.skill) {
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: `No \`skill\` given, but you passed \`args\` — this looks like an invocation that forgot to name the skill. Set \`skill\` to one of the names below, or call with \`list: true\` to just browse.\n\nAvailable skills:\n${describe()}`,
111
+ },
112
+ ],
113
+ details: {} as Record<string, unknown>,
114
+ isError: true,
115
+ };
116
+ }
117
+
118
+ const wanted = params.skill.replace(/^\//, "");
119
+ const all = index();
120
+ const found =
121
+ all.find((skill) => skill.name === wanted) ??
122
+ all.find((skill) => skill.name.toLowerCase() === wanted.toLowerCase()) ??
123
+ // A bare name matches a plugin skill when it is unambiguous.
124
+ all.find((skill) => skill.name.endsWith(`:${wanted}`));
125
+
126
+ if (!found) {
127
+ return {
128
+ content: [{ type: "text", text: `No skill named "${params.skill}".\n\nAvailable skills:\n${describe()}` }],
129
+ details: {} as Record<string, unknown>,
130
+ isError: true,
131
+ };
132
+ }
133
+
134
+ let body: string;
135
+ try {
136
+ const parsed = parseFrontmatter(readFileSync(found.path, "utf-8")) as { body: string };
137
+ body = parsed.body.trim();
138
+ } catch (error) {
139
+ return {
140
+ content: [{ type: "text", text: `Could not read skill "${found.name}": ${(error as Error).message}` }],
141
+ details: {} as Record<string, unknown>,
142
+ isError: true,
143
+ };
144
+ }
145
+
146
+ // Resource paths in a skill are relative to its own directory, so the
147
+ // model needs to know where it lives to read references/ or scripts/.
148
+ const header = [
149
+ `Skill: ${found.name}`,
150
+ `Location: ${found.path}`,
151
+ params.args ? `Arguments: ${params.args}` : undefined,
152
+ "Follow these instructions for the current task.",
153
+ ]
154
+ .filter(Boolean)
155
+ .join("\n");
156
+
157
+ return {
158
+ content: [{ type: "text", text: `${header}\n\n---\n\n${body}` }],
159
+ details: { skill: found.name, path: found.path } as Record<string, unknown>,
160
+ };
161
+ },
162
+ });
163
+
164
+ pi.registerCommand("skills", {
165
+ description: "List available skills",
166
+ handler: async (_args, ctx) => {
167
+ ctx.ui.notify(`Available skills:\n${describe()}`, "info");
168
+ },
169
+ });
170
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Agent definition discovery (pure) — Claude Code's `.claude/agents/*.md`
3
+ * layout: YAML frontmatter (name, description, tools, model) plus a system
4
+ * prompt body.
5
+ *
6
+ * Precedence: user (~/.claude/agents) < project (<cwd>/.claude/agents).
7
+ * Subdirectories are searched recursively.
8
+ */
9
+
10
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
11
+ import { basename, join } from "node:path";
12
+ import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
13
+
14
+ export interface AgentDefinition {
15
+ name: string;
16
+ description: string;
17
+ /** Tool allowlist for the child; undefined means the child's defaults. */
18
+ tools?: string[];
19
+ /** Model override (pi model id, e.g. "anthropic/claude-sonnet-5"). */
20
+ model?: string;
21
+ systemPrompt: string;
22
+ source: string;
23
+ }
24
+
25
+ function collectMarkdownFiles(dir: string, out: string[] = []): string[] {
26
+ if (!existsSync(dir)) return out;
27
+ for (const entry of readdirSync(dir)) {
28
+ const full = join(dir, entry);
29
+ if (statSync(full).isDirectory()) {
30
+ collectMarkdownFiles(full, out);
31
+ } else if (entry.endsWith(".md")) {
32
+ out.push(full);
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /**
39
+ * Frontmatter in the wild is not always valid YAML: real Claude Code plugin
40
+ * agents contain unquoted descriptions with `: ` in them, which pi's parser
41
+ * rejects ("Nested mappings are not allowed in compact mappings"). Falling back
42
+ * to line-wise extraction keeps those definitions usable instead of dropping
43
+ * them silently.
44
+ */
45
+ function parseFrontmatterLoosely(content: string): { frontmatter: Record<string, unknown>; body: string } {
46
+ try {
47
+ const parsed = parseFrontmatter(content) as { frontmatter?: Record<string, unknown>; body: string };
48
+ return { frontmatter: parsed.frontmatter ?? {}, body: parsed.body };
49
+ } catch {
50
+ // Fall through to the lenient path.
51
+ }
52
+
53
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
54
+ if (!match) return { frontmatter: {}, body: content };
55
+
56
+ const frontmatter: Record<string, unknown> = {};
57
+ for (const line of match[1].split(/\r?\n/)) {
58
+ const keyValue = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
59
+ if (!keyValue) continue;
60
+ const value = keyValue[2].trim().replace(/^["']|["']$/g, "");
61
+ if (value) frontmatter[keyValue[1]] = value;
62
+ }
63
+ return { frontmatter, body: match[2] };
64
+ }
65
+
66
+ export function parseAgentFile(path: string, content: string): AgentDefinition | undefined {
67
+ const { frontmatter, body } = parseFrontmatterLoosely(content);
68
+ const fm = frontmatter;
69
+ const name = typeof fm.name === "string" && fm.name.trim() ? fm.name.trim() : basename(path, ".md");
70
+ if (!body.trim()) return undefined;
71
+
72
+ const rawTools = fm.tools;
73
+ const tools =
74
+ typeof rawTools === "string"
75
+ ? rawTools
76
+ .split(",")
77
+ .map((t) => t.trim())
78
+ .filter(Boolean)
79
+ : Array.isArray(rawTools)
80
+ ? rawTools.filter((t): t is string => typeof t === "string")
81
+ : undefined;
82
+
83
+ return {
84
+ name,
85
+ description: typeof fm.description === "string" ? fm.description : "",
86
+ tools: tools && tools.length > 0 ? tools : undefined,
87
+ // "inherit" is Claude Code's way of saying "use the session model".
88
+ model: typeof fm.model === "string" && fm.model !== "inherit" ? fm.model : undefined,
89
+ systemPrompt: body.trim(),
90
+ source: path,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Lowest to highest precedence: the catalog bundled with this package, then the
96
+ * user's `~/.claude/agents`, then the project's `.claude/agents`. A user or
97
+ * project definition with the same name replaces a bundled one.
98
+ */
99
+ export function agentDirs(cwd: string, home: string, bundled?: string): string[] {
100
+ return [...(bundled ? [bundled] : []), join(home, ".claude", "agents"), join(cwd, ".claude", "agents")];
101
+ }
102
+
103
+ /** A directory whose agents are exposed as `<namespace>:<name>` (plugins). */
104
+ export interface AgentSource {
105
+ dir: string;
106
+ namespace?: string;
107
+ }
108
+
109
+ /** Later sources override earlier ones on name collisions. */
110
+ export function discoverAgents(sources: Array<string | AgentSource>): AgentDefinition[] {
111
+ const byName = new Map<string, AgentDefinition>();
112
+ for (const source of sources) {
113
+ const { dir, namespace } = typeof source === "string" ? { dir: source, namespace: undefined } : source;
114
+ for (const file of collectMarkdownFiles(dir)) {
115
+ try {
116
+ const agent = parseAgentFile(file, readFileSync(file, "utf-8"));
117
+ if (!agent) continue;
118
+ const name = namespace ? `${namespace}:${agent.name}` : agent.name;
119
+ byName.set(name, { ...agent, name });
120
+ } catch {
121
+ // Unreadable or malformed definition: skip it rather than failing discovery.
122
+ }
123
+ }
124
+ }
125
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
126
+ }