edge-pi-cli 0.1.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 (117) hide show
  1. package/dist/auth/anthropic-oauth.d.ts +10 -0
  2. package/dist/auth/anthropic-oauth.d.ts.map +1 -0
  3. package/dist/auth/anthropic-oauth.js +97 -0
  4. package/dist/auth/anthropic-oauth.js.map +1 -0
  5. package/dist/auth/auth-storage.d.ts +46 -0
  6. package/dist/auth/auth-storage.d.ts.map +1 -0
  7. package/dist/auth/auth-storage.js +213 -0
  8. package/dist/auth/auth-storage.js.map +1 -0
  9. package/dist/auth/github-copilot-oauth.d.ts +8 -0
  10. package/dist/auth/github-copilot-oauth.d.ts.map +1 -0
  11. package/dist/auth/github-copilot-oauth.js +131 -0
  12. package/dist/auth/github-copilot-oauth.js.map +1 -0
  13. package/dist/auth/index.d.ts +6 -0
  14. package/dist/auth/index.d.ts.map +1 -0
  15. package/dist/auth/index.js +5 -0
  16. package/dist/auth/index.js.map +1 -0
  17. package/dist/auth/openai-codex-oauth.d.ts +8 -0
  18. package/dist/auth/openai-codex-oauth.d.ts.map +1 -0
  19. package/dist/auth/openai-codex-oauth.js +131 -0
  20. package/dist/auth/openai-codex-oauth.js.map +1 -0
  21. package/dist/auth/types.d.ts +41 -0
  22. package/dist/auth/types.d.ts.map +1 -0
  23. package/dist/auth/types.js +5 -0
  24. package/dist/auth/types.js.map +1 -0
  25. package/dist/cli/args.d.ts +35 -0
  26. package/dist/cli/args.d.ts.map +1 -0
  27. package/dist/cli/args.js +191 -0
  28. package/dist/cli/args.js.map +1 -0
  29. package/dist/cli.d.ts +3 -0
  30. package/dist/cli.d.ts.map +1 -0
  31. package/dist/cli.js +8 -0
  32. package/dist/cli.js.map +1 -0
  33. package/dist/context.d.ts +16 -0
  34. package/dist/context.d.ts.map +1 -0
  35. package/dist/context.js +38 -0
  36. package/dist/context.js.map +1 -0
  37. package/dist/main.d.ts +8 -0
  38. package/dist/main.d.ts.map +1 -0
  39. package/dist/main.js +313 -0
  40. package/dist/main.js.map +1 -0
  41. package/dist/model-factory.d.ts +45 -0
  42. package/dist/model-factory.d.ts.map +1 -0
  43. package/dist/model-factory.js +175 -0
  44. package/dist/model-factory.js.map +1 -0
  45. package/dist/modes/interactive/bash-helpers.d.ts +31 -0
  46. package/dist/modes/interactive/bash-helpers.d.ts.map +1 -0
  47. package/dist/modes/interactive/bash-helpers.js +68 -0
  48. package/dist/modes/interactive/bash-helpers.js.map +1 -0
  49. package/dist/modes/interactive/components/assistant-message.d.ts +19 -0
  50. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -0
  51. package/dist/modes/interactive/components/assistant-message.js +54 -0
  52. package/dist/modes/interactive/components/assistant-message.js.map +1 -0
  53. package/dist/modes/interactive/components/bash-execution.d.ts +18 -0
  54. package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -0
  55. package/dist/modes/interactive/components/bash-execution.js +77 -0
  56. package/dist/modes/interactive/components/bash-execution.js.map +1 -0
  57. package/dist/modes/interactive/components/compaction-summary.d.ts +18 -0
  58. package/dist/modes/interactive/components/compaction-summary.d.ts.map +1 -0
  59. package/dist/modes/interactive/components/compaction-summary.js +45 -0
  60. package/dist/modes/interactive/components/compaction-summary.js.map +1 -0
  61. package/dist/modes/interactive/components/footer.d.ts +20 -0
  62. package/dist/modes/interactive/components/footer.d.ts.map +1 -0
  63. package/dist/modes/interactive/components/footer.js +82 -0
  64. package/dist/modes/interactive/components/footer.js.map +1 -0
  65. package/dist/modes/interactive/components/tool-execution.d.ts +30 -0
  66. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -0
  67. package/dist/modes/interactive/components/tool-execution.js +133 -0
  68. package/dist/modes/interactive/components/tool-execution.js.map +1 -0
  69. package/dist/modes/interactive/components/user-message.d.ts +9 -0
  70. package/dist/modes/interactive/components/user-message.d.ts.map +1 -0
  71. package/dist/modes/interactive/components/user-message.js +17 -0
  72. package/dist/modes/interactive/components/user-message.js.map +1 -0
  73. package/dist/modes/interactive/interactive-mode.d.ts +49 -0
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -0
  75. package/dist/modes/interactive/interactive-mode.js +1397 -0
  76. package/dist/modes/interactive/interactive-mode.js.map +1 -0
  77. package/dist/modes/interactive/theme.d.ts +26 -0
  78. package/dist/modes/interactive/theme.d.ts.map +1 -0
  79. package/dist/modes/interactive/theme.js +64 -0
  80. package/dist/modes/interactive/theme.js.map +1 -0
  81. package/dist/modes/interactive-mode.d.ts +5 -0
  82. package/dist/modes/interactive-mode.d.ts.map +1 -0
  83. package/dist/modes/interactive-mode.js +5 -0
  84. package/dist/modes/interactive-mode.js.map +1 -0
  85. package/dist/modes/print-mode.d.ts +20 -0
  86. package/dist/modes/print-mode.d.ts.map +1 -0
  87. package/dist/modes/print-mode.js +56 -0
  88. package/dist/modes/print-mode.js.map +1 -0
  89. package/dist/prompts.d.ts +53 -0
  90. package/dist/prompts.d.ts.map +1 -0
  91. package/dist/prompts.js +132 -0
  92. package/dist/prompts.js.map +1 -0
  93. package/dist/settings.d.ts +34 -0
  94. package/dist/settings.d.ts.map +1 -0
  95. package/dist/settings.js +73 -0
  96. package/dist/settings.js.map +1 -0
  97. package/dist/skills.d.ts +51 -0
  98. package/dist/skills.d.ts.map +1 -0
  99. package/dist/skills.js +304 -0
  100. package/dist/skills.js.map +1 -0
  101. package/dist/utils/bash-executor.d.ts +32 -0
  102. package/dist/utils/bash-executor.d.ts.map +1 -0
  103. package/dist/utils/bash-executor.js +166 -0
  104. package/dist/utils/bash-executor.js.map +1 -0
  105. package/dist/utils/clipboard-image.d.ts +24 -0
  106. package/dist/utils/clipboard-image.d.ts.map +1 -0
  107. package/dist/utils/clipboard-image.js +211 -0
  108. package/dist/utils/clipboard-image.js.map +1 -0
  109. package/dist/utils/find-fd.d.ts +12 -0
  110. package/dist/utils/find-fd.d.ts.map +1 -0
  111. package/dist/utils/find-fd.js +33 -0
  112. package/dist/utils/find-fd.js.map +1 -0
  113. package/dist/utils/frontmatter.d.ts +7 -0
  114. package/dist/utils/frontmatter.d.ts.map +1 -0
  115. package/dist/utils/frontmatter.js +25 -0
  116. package/dist/utils/frontmatter.js.map +1 -0
  117. package/package.json +39 -0
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Prompt template loading and expansion.
3
+ *
4
+ * Loads prompt templates from:
5
+ * - ~/.pi/agent/prompts/ (user-level)
6
+ * - .pi/prompts/ (project-level)
7
+ *
8
+ * Prompt templates are markdown files with frontmatter containing a
9
+ * "description" field. The filename (without .md) becomes the command name.
10
+ * They are invoked via /name in the editor and expand to the file body.
11
+ */
12
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
13
+ import { homedir } from "node:os";
14
+ import { basename, join, resolve } from "node:path";
15
+ import { parseFrontmatter } from "./utils/frontmatter.js";
16
+ const CONFIG_DIR_NAME = ".pi";
17
+ function getDefaultAgentDir() {
18
+ const envDir = process.env.PI_CODING_AGENT_DIR;
19
+ if (envDir) {
20
+ if (envDir === "~")
21
+ return homedir();
22
+ if (envDir.startsWith("~/"))
23
+ return homedir() + envDir.slice(1);
24
+ return envDir;
25
+ }
26
+ return join(homedir(), CONFIG_DIR_NAME, "agent");
27
+ }
28
+ function loadPromptsFromDir(dir, source) {
29
+ const prompts = [];
30
+ const diagnostics = [];
31
+ if (!existsSync(dir)) {
32
+ return { prompts, diagnostics };
33
+ }
34
+ try {
35
+ const entries = readdirSync(dir, { withFileTypes: true });
36
+ for (const entry of entries) {
37
+ if (entry.name.startsWith(".") || !entry.name.endsWith(".md"))
38
+ continue;
39
+ const fullPath = join(dir, entry.name);
40
+ let isFile = entry.isFile();
41
+ if (entry.isSymbolicLink()) {
42
+ try {
43
+ isFile = statSync(fullPath).isFile();
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ }
49
+ if (!isFile)
50
+ continue;
51
+ try {
52
+ const rawContent = readFileSync(fullPath, "utf-8");
53
+ const { frontmatter, body } = parseFrontmatter(rawContent);
54
+ const name = basename(entry.name, ".md");
55
+ if (!frontmatter.description || frontmatter.description.trim() === "") {
56
+ diagnostics.push({
57
+ type: "warning",
58
+ message: "prompt template missing description in frontmatter",
59
+ path: fullPath,
60
+ });
61
+ continue;
62
+ }
63
+ prompts.push({
64
+ name,
65
+ description: frontmatter.description,
66
+ filePath: fullPath,
67
+ body: body.trim(),
68
+ source,
69
+ });
70
+ }
71
+ catch (error) {
72
+ const message = error instanceof Error ? error.message : "failed to parse prompt template";
73
+ diagnostics.push({ type: "warning", message, path: fullPath });
74
+ }
75
+ }
76
+ }
77
+ catch {
78
+ // directory not readable
79
+ }
80
+ return { prompts, diagnostics };
81
+ }
82
+ /**
83
+ * Load prompt templates from user and project directories.
84
+ * Project prompts override user prompts with the same name.
85
+ */
86
+ export function loadPrompts(options = {}) {
87
+ const { cwd = process.cwd(), agentDir } = options;
88
+ const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
89
+ const allDiagnostics = [];
90
+ const promptMap = new Map();
91
+ // Load user-level prompts first
92
+ const userResult = loadPromptsFromDir(join(resolvedAgentDir, "prompts"), "user");
93
+ allDiagnostics.push(...userResult.diagnostics);
94
+ for (const prompt of userResult.prompts) {
95
+ promptMap.set(prompt.name, prompt);
96
+ }
97
+ // Load project-level prompts (override user-level)
98
+ const projectResult = loadPromptsFromDir(resolve(cwd, CONFIG_DIR_NAME, "prompts"), "project");
99
+ allDiagnostics.push(...projectResult.diagnostics);
100
+ for (const prompt of projectResult.prompts) {
101
+ if (promptMap.has(prompt.name)) {
102
+ allDiagnostics.push({
103
+ type: "collision",
104
+ message: `prompt "${prompt.name}" overrides user-level prompt`,
105
+ path: prompt.filePath,
106
+ });
107
+ }
108
+ promptMap.set(prompt.name, prompt);
109
+ }
110
+ return {
111
+ prompts: Array.from(promptMap.values()),
112
+ diagnostics: allDiagnostics,
113
+ };
114
+ }
115
+ /**
116
+ * Expand prompt template references in user input.
117
+ * If the input starts with /name and matches a template, replace with the template body.
118
+ * Returns the original text if no template matches.
119
+ */
120
+ export function expandPromptTemplate(text, templates) {
121
+ if (!text.startsWith("/"))
122
+ return text;
123
+ // Extract command name (first word after /)
124
+ const spaceIndex = text.indexOf(" ");
125
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
126
+ const remainder = spaceIndex === -1 ? "" : text.slice(spaceIndex);
127
+ const template = templates.find((t) => t.name === commandName);
128
+ if (!template)
129
+ return text;
130
+ return template.body + remainder;
131
+ }
132
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Prompt template loading and expansion.\n *\n * Loads prompt templates from:\n * - ~/.pi/agent/prompts/ (user-level)\n * - .pi/prompts/ (project-level)\n *\n * Prompt templates are markdown files with frontmatter containing a\n * \"description\" field. The filename (without .md) becomes the command name.\n * They are invoked via /name in the editor and expand to the file body.\n */\n\nimport { existsSync, readdirSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, join, resolve } from \"node:path\";\nimport { parseFrontmatter } from \"./utils/frontmatter.js\";\n\nconst CONFIG_DIR_NAME = \".pi\";\n\nexport interface PromptTemplate {\n\t/** Command name (filename without .md extension) */\n\tname: string;\n\t/** Description from frontmatter */\n\tdescription: string;\n\t/** Full file path */\n\tfilePath: string;\n\t/** The body content (after frontmatter) */\n\tbody: string;\n\t/** Source: \"user\" or \"project\" */\n\tsource: \"user\" | \"project\";\n}\n\nexport interface PromptFrontmatter {\n\tdescription?: string;\n\t[key: string]: unknown;\n}\n\nexport interface LoadPromptsResult {\n\tprompts: PromptTemplate[];\n\tdiagnostics: Array<{ type: \"warning\" | \"collision\"; message: string; path: string }>;\n}\n\nfunction getDefaultAgentDir(): string {\n\tconst envDir = process.env.PI_CODING_AGENT_DIR;\n\tif (envDir) {\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\treturn join(homedir(), CONFIG_DIR_NAME, \"agent\");\n}\n\nfunction loadPromptsFromDir(dir: string, source: \"user\" | \"project\"): LoadPromptsResult {\n\tconst prompts: PromptTemplate[] = [];\n\tconst diagnostics: LoadPromptsResult[\"diagnostics\"] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn { prompts, diagnostics };\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\") || !entry.name.endsWith(\".md\")) continue;\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tisFile = statSync(fullPath).isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!isFile) continue;\n\n\t\t\ttry {\n\t\t\t\tconst rawContent = readFileSync(fullPath, \"utf-8\");\n\t\t\t\tconst { frontmatter, body } = parseFrontmatter<PromptFrontmatter>(rawContent);\n\t\t\t\tconst name = basename(entry.name, \".md\");\n\n\t\t\t\tif (!frontmatter.description || frontmatter.description.trim() === \"\") {\n\t\t\t\t\tdiagnostics.push({\n\t\t\t\t\t\ttype: \"warning\",\n\t\t\t\t\t\tmessage: \"prompt template missing description in frontmatter\",\n\t\t\t\t\t\tpath: fullPath,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tprompts.push({\n\t\t\t\t\tname,\n\t\t\t\t\tdescription: frontmatter.description,\n\t\t\t\t\tfilePath: fullPath,\n\t\t\t\t\tbody: body.trim(),\n\t\t\t\t\tsource,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : \"failed to parse prompt template\";\n\t\t\t\tdiagnostics.push({ type: \"warning\", message, path: fullPath });\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// directory not readable\n\t}\n\n\treturn { prompts, diagnostics };\n}\n\nexport interface LoadPromptsOptions {\n\t/** Working directory for project-local prompts. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory. Default: ~/.pi/agent */\n\tagentDir?: string;\n}\n\n/**\n * Load prompt templates from user and project directories.\n * Project prompts override user prompts with the same name.\n */\nexport function loadPrompts(options: LoadPromptsOptions = {}): LoadPromptsResult {\n\tconst { cwd = process.cwd(), agentDir } = options;\n\tconst resolvedAgentDir = agentDir ?? getDefaultAgentDir();\n\n\tconst allDiagnostics: LoadPromptsResult[\"diagnostics\"] = [];\n\tconst promptMap = new Map<string, PromptTemplate>();\n\n\t// Load user-level prompts first\n\tconst userResult = loadPromptsFromDir(join(resolvedAgentDir, \"prompts\"), \"user\");\n\tallDiagnostics.push(...userResult.diagnostics);\n\tfor (const prompt of userResult.prompts) {\n\t\tpromptMap.set(prompt.name, prompt);\n\t}\n\n\t// Load project-level prompts (override user-level)\n\tconst projectResult = loadPromptsFromDir(resolve(cwd, CONFIG_DIR_NAME, \"prompts\"), \"project\");\n\tallDiagnostics.push(...projectResult.diagnostics);\n\tfor (const prompt of projectResult.prompts) {\n\t\tif (promptMap.has(prompt.name)) {\n\t\t\tallDiagnostics.push({\n\t\t\t\ttype: \"collision\",\n\t\t\t\tmessage: `prompt \"${prompt.name}\" overrides user-level prompt`,\n\t\t\t\tpath: prompt.filePath,\n\t\t\t});\n\t\t}\n\t\tpromptMap.set(prompt.name, prompt);\n\t}\n\n\treturn {\n\t\tprompts: Array.from(promptMap.values()),\n\t\tdiagnostics: allDiagnostics,\n\t};\n}\n\n/**\n * Expand prompt template references in user input.\n * If the input starts with /name and matches a template, replace with the template body.\n * Returns the original text if no template matches.\n */\nexport function expandPromptTemplate(text: string, templates: PromptTemplate[]): string {\n\tif (!text.startsWith(\"/\")) return text;\n\n\t// Extract command name (first word after /)\n\tconst spaceIndex = text.indexOf(\" \");\n\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\tconst remainder = spaceIndex === -1 ? \"\" : text.slice(spaceIndex);\n\n\tconst template = templates.find((t) => t.name === commandName);\n\tif (!template) return text;\n\n\treturn template.body + remainder;\n}\n"]}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Settings persistence for edge-pi-cli.
3
+ *
4
+ * Stores user preferences (default provider/model, compaction settings)
5
+ * in ~/.pi/agent/settings.json. Follows the same file-based pattern
6
+ * as auth-storage.ts: load in constructor, save on mutation, create dir if needed.
7
+ */
8
+ export interface CompactionSettings {
9
+ enabled?: boolean;
10
+ reserveTokens?: number;
11
+ keepRecentTokens?: number;
12
+ }
13
+ export interface SettingsData {
14
+ defaultProvider?: string;
15
+ defaultModel?: string;
16
+ compaction?: CompactionSettings;
17
+ }
18
+ export declare class SettingsManager {
19
+ private settingsPath;
20
+ private data;
21
+ private constructor();
22
+ static create(agentDir: string): SettingsManager;
23
+ private reload;
24
+ private save;
25
+ getDefaultProvider(): string | undefined;
26
+ setDefaultProvider(provider: string): void;
27
+ getDefaultModel(): string | undefined;
28
+ setDefaultModel(model: string): void;
29
+ getCompaction(): CompactionSettings | undefined;
30
+ getCompactionEnabled(): boolean;
31
+ setCompactionEnabled(enabled: boolean): void;
32
+ setDefaults(provider: string, model: string): void;
33
+ }
34
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,kBAAkB,CAAC;CAChC;AAED,qBAAa,eAAe;IAGP,OAAO,CAAC,YAAY;IAFxC,OAAO,CAAC,IAAI,CAAoB;IAEhC,OAAO,eAEN;IAED,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAG/C;IAED,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,IAAI;IAQZ,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAEvC;IAED,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAGzC;IAED,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAGnC;IAED,aAAa,IAAI,kBAAkB,GAAG,SAAS,CAE9C;IAED,oBAAoB,IAAI,OAAO,CAE9B;IAED,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAM3C;IAED,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAIjD;CACD","sourcesContent":["/**\n * Settings persistence for edge-pi-cli.\n *\n * Stores user preferences (default provider/model, compaction settings)\n * in ~/.pi/agent/settings.json. Follows the same file-based pattern\n * as auth-storage.ts: load in constructor, save on mutation, create dir if needed.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean;\n\treserveTokens?: number;\n\tkeepRecentTokens?: number;\n}\n\nexport interface SettingsData {\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tcompaction?: CompactionSettings;\n}\n\nexport class SettingsManager {\n\tprivate data: SettingsData = {};\n\n\tprivate constructor(private settingsPath: string) {\n\t\tthis.reload();\n\t}\n\n\tstatic create(agentDir: string): SettingsManager {\n\t\tconst settingsPath = `${agentDir}/settings.json`;\n\t\treturn new SettingsManager(settingsPath);\n\t}\n\n\tprivate reload(): void {\n\t\tif (!existsSync(this.settingsPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.settingsPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\tprivate save(): void {\n\t\tconst dir = dirname(this.settingsPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.settingsPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.data.defaultProvider;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.data.defaultProvider = provider;\n\t\tthis.save();\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.data.defaultModel;\n\t}\n\n\tsetDefaultModel(model: string): void {\n\t\tthis.data.defaultModel = model;\n\t\tthis.save();\n\t}\n\n\tgetCompaction(): CompactionSettings | undefined {\n\t\treturn this.data.compaction;\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.data.compaction?.enabled ?? true;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.data.compaction) {\n\t\t\tthis.data.compaction = {};\n\t\t}\n\t\tthis.data.compaction.enabled = enabled;\n\t\tthis.save();\n\t}\n\n\tsetDefaults(provider: string, model: string): void {\n\t\tthis.data.defaultProvider = provider;\n\t\tthis.data.defaultModel = model;\n\t\tthis.save();\n\t}\n}\n"]}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Settings persistence for edge-pi-cli.
3
+ *
4
+ * Stores user preferences (default provider/model, compaction settings)
5
+ * in ~/.pi/agent/settings.json. Follows the same file-based pattern
6
+ * as auth-storage.ts: load in constructor, save on mutation, create dir if needed.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { dirname } from "node:path";
10
+ export class SettingsManager {
11
+ settingsPath;
12
+ data = {};
13
+ constructor(settingsPath) {
14
+ this.settingsPath = settingsPath;
15
+ this.reload();
16
+ }
17
+ static create(agentDir) {
18
+ const settingsPath = `${agentDir}/settings.json`;
19
+ return new SettingsManager(settingsPath);
20
+ }
21
+ reload() {
22
+ if (!existsSync(this.settingsPath)) {
23
+ this.data = {};
24
+ return;
25
+ }
26
+ try {
27
+ this.data = JSON.parse(readFileSync(this.settingsPath, "utf-8"));
28
+ }
29
+ catch {
30
+ this.data = {};
31
+ }
32
+ }
33
+ save() {
34
+ const dir = dirname(this.settingsPath);
35
+ if (!existsSync(dir)) {
36
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
37
+ }
38
+ writeFileSync(this.settingsPath, JSON.stringify(this.data, null, 2), "utf-8");
39
+ }
40
+ getDefaultProvider() {
41
+ return this.data.defaultProvider;
42
+ }
43
+ setDefaultProvider(provider) {
44
+ this.data.defaultProvider = provider;
45
+ this.save();
46
+ }
47
+ getDefaultModel() {
48
+ return this.data.defaultModel;
49
+ }
50
+ setDefaultModel(model) {
51
+ this.data.defaultModel = model;
52
+ this.save();
53
+ }
54
+ getCompaction() {
55
+ return this.data.compaction;
56
+ }
57
+ getCompactionEnabled() {
58
+ return this.data.compaction?.enabled ?? true;
59
+ }
60
+ setCompactionEnabled(enabled) {
61
+ if (!this.data.compaction) {
62
+ this.data.compaction = {};
63
+ }
64
+ this.data.compaction.enabled = enabled;
65
+ this.save();
66
+ }
67
+ setDefaults(provider, model) {
68
+ this.data.defaultProvider = provider;
69
+ this.data.defaultModel = model;
70
+ this.save();
71
+ }
72
+ }
73
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAcpC,MAAM,OAAO,eAAe;IAGC,YAAY;IAFhC,IAAI,GAAiB,EAAE,CAAC;IAEhC,YAA4B,YAAoB,EAAE;4BAAtB,YAAY;QACvC,IAAI,CAAC,MAAM,EAAE,CAAC;IAAA,CACd;IAED,MAAM,CAAC,MAAM,CAAC,QAAgB,EAAmB;QAChD,MAAM,YAAY,GAAG,GAAG,QAAQ,gBAAgB,CAAC;QACjD,OAAO,IAAI,eAAe,CAAC,YAAY,CAAC,CAAC;IAAA,CACzC;IAEO,MAAM,GAAS;QACtB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;YACpC,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QACD,IAAI,CAAC;YACJ,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;QAClE,CAAC;QAAC,MAAM,CAAC;YACR,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;QAChB,CAAC;IAAA,CACD;IAEO,IAAI,GAAS;QACpB,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACvC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAAA,CAC9E;IAED,kBAAkB,GAAuB;QACxC,OAAO,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC;IAAA,CACjC;IAED,kBAAkB,CAAC,QAAgB,EAAQ;QAC1C,IAAI,CAAC,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC;QACrC,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,eAAe,GAAuB;QACrC,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC;IAAA,CAC9B;IAED,eAAe,CAAC,KAAa,EAAQ;QACpC,IAAI,CAAC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC/B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,aAAa,GAAmC;QAC/C,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC;IAAA,CAC5B;IAED,oBAAoB,GAAY;QAC/B,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,IAAI,IAAI,CAAC;IAAA,CAC7C;IAED,oBAAoB,CAAC,OAAgB,EAAQ;QAC5C,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,EAAE,CAAC;QAC3B,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;QACvC,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;IAED,WAAW,CAAC,QAAgB,EAAE,KAAa,EAAQ;QAClD,IAAI,CAAC,IAAI,CAAC,eAAe,GAAG,QAAQ,CAAC;QACrC,IAAI,CAAC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;QAC/B,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;CACD","sourcesContent":["/**\n * Settings persistence for edge-pi-cli.\n *\n * Stores user preferences (default provider/model, compaction settings)\n * in ~/.pi/agent/settings.json. Follows the same file-based pattern\n * as auth-storage.ts: load in constructor, save on mutation, create dir if needed.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean;\n\treserveTokens?: number;\n\tkeepRecentTokens?: number;\n}\n\nexport interface SettingsData {\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tcompaction?: CompactionSettings;\n}\n\nexport class SettingsManager {\n\tprivate data: SettingsData = {};\n\n\tprivate constructor(private settingsPath: string) {\n\t\tthis.reload();\n\t}\n\n\tstatic create(agentDir: string): SettingsManager {\n\t\tconst settingsPath = `${agentDir}/settings.json`;\n\t\treturn new SettingsManager(settingsPath);\n\t}\n\n\tprivate reload(): void {\n\t\tif (!existsSync(this.settingsPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.settingsPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\tprivate save(): void {\n\t\tconst dir = dirname(this.settingsPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.settingsPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.data.defaultProvider;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.data.defaultProvider = provider;\n\t\tthis.save();\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.data.defaultModel;\n\t}\n\n\tsetDefaultModel(model: string): void {\n\t\tthis.data.defaultModel = model;\n\t\tthis.save();\n\t}\n\n\tgetCompaction(): CompactionSettings | undefined {\n\t\treturn this.data.compaction;\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.data.compaction?.enabled ?? true;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.data.compaction) {\n\t\t\tthis.data.compaction = {};\n\t\t}\n\t\tthis.data.compaction.enabled = enabled;\n\t\tthis.save();\n\t}\n\n\tsetDefaults(provider: string, model: string): void {\n\t\tthis.data.defaultProvider = provider;\n\t\tthis.data.defaultModel = model;\n\t\tthis.save();\n\t}\n}\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Skill loading and formatting for the system prompt.
3
+ * Implements the Agent Skills spec: https://agentskills.io/specification
4
+ *
5
+ * Standalone implementation - no dependency on the old SDK.
6
+ */
7
+ export interface SkillFrontmatter {
8
+ name?: string;
9
+ description?: string;
10
+ "disable-model-invocation"?: boolean;
11
+ [key: string]: unknown;
12
+ }
13
+ export interface Skill {
14
+ name: string;
15
+ description: string;
16
+ filePath: string;
17
+ baseDir: string;
18
+ source: string;
19
+ disableModelInvocation: boolean;
20
+ }
21
+ export interface SkillDiagnostic {
22
+ type: "warning" | "collision";
23
+ message: string;
24
+ path: string;
25
+ }
26
+ export interface LoadSkillsResult {
27
+ skills: Skill[];
28
+ diagnostics: SkillDiagnostic[];
29
+ }
30
+ export interface LoadSkillsOptions {
31
+ /** Working directory for project-local skills. Default: process.cwd() */
32
+ cwd?: string;
33
+ /** Agent config directory for global skills. Default: ~/.pi/agent */
34
+ agentDir?: string;
35
+ /** Explicit skill paths (files or directories) */
36
+ skillPaths?: string[];
37
+ /** Include default skill directories. Default: true */
38
+ includeDefaults?: boolean;
39
+ }
40
+ /**
41
+ * Load skills from all configured locations.
42
+ */
43
+ export declare function loadSkills(options?: LoadSkillsOptions): LoadSkillsResult;
44
+ /**
45
+ * Format skills for inclusion in a system prompt.
46
+ * Uses XML format per Agent Skills standard.
47
+ *
48
+ * Skills with disableModelInvocation=true are excluded from the prompt.
49
+ */
50
+ export declare function formatSkillsForPrompt(skills: Skill[]): string;
51
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsBH,MAAM,WAAW,gBAAgB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,KAAK;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,sBAAsB,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,SAAS,GAAG,WAAW,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,gBAAgB;IAChC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,WAAW,EAAE,eAAe,EAAE,CAAC;CAC/B;AAiJD,MAAM,WAAW,iBAAiB;IACjC,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,uDAAuD;IACvD,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAyBD;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CA0F5E;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CA0B7D","sourcesContent":["/**\n * Skill loading and formatting for the system prompt.\n * Implements the Agent Skills spec: https://agentskills.io/specification\n *\n * Standalone implementation - no dependency on the old SDK.\n */\n\nimport { existsSync, readdirSync, readFileSync, realpathSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { basename, dirname, isAbsolute, join, resolve, sep } from \"node:path\";\nimport { parseFrontmatter } from \"./utils/frontmatter.js\";\n\nconst CONFIG_DIR_NAME = \".pi\";\n\nconst ALLOWED_FRONTMATTER_FIELDS = new Set([\n\t\"name\",\n\t\"description\",\n\t\"license\",\n\t\"compatibility\",\n\t\"metadata\",\n\t\"allowed-tools\",\n\t\"disable-model-invocation\",\n]);\n\nconst MAX_NAME_LENGTH = 64;\nconst MAX_DESCRIPTION_LENGTH = 1024;\n\nexport interface SkillFrontmatter {\n\tname?: string;\n\tdescription?: string;\n\t\"disable-model-invocation\"?: boolean;\n\t[key: string]: unknown;\n}\n\nexport interface Skill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsource: string;\n\tdisableModelInvocation: boolean;\n}\n\nexport interface SkillDiagnostic {\n\ttype: \"warning\" | \"collision\";\n\tmessage: string;\n\tpath: string;\n}\n\nexport interface LoadSkillsResult {\n\tskills: Skill[];\n\tdiagnostics: SkillDiagnostic[];\n}\n\nfunction validateName(name: string, parentDirName: string): string[] {\n\tconst errors: string[] = [];\n\tif (name !== parentDirName) {\n\t\terrors.push(`name \"${name}\" does not match parent directory \"${parentDirName}\"`);\n\t}\n\tif (name.length > MAX_NAME_LENGTH) {\n\t\terrors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);\n\t}\n\tif (!/^[a-z0-9-]+$/.test(name)) {\n\t\terrors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);\n\t}\n\tif (name.startsWith(\"-\") || name.endsWith(\"-\")) {\n\t\terrors.push(`name must not start or end with a hyphen`);\n\t}\n\tif (name.includes(\"--\")) {\n\t\terrors.push(`name must not contain consecutive hyphens`);\n\t}\n\treturn errors;\n}\n\nfunction validateDescription(description: string | undefined): string[] {\n\tconst errors: string[] = [];\n\tif (!description || description.trim() === \"\") {\n\t\terrors.push(\"description is required\");\n\t} else if (description.length > MAX_DESCRIPTION_LENGTH) {\n\t\terrors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);\n\t}\n\treturn errors;\n}\n\nfunction validateFrontmatterFields(keys: string[]): string[] {\n\tconst errors: string[] = [];\n\tfor (const key of keys) {\n\t\tif (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {\n\t\t\terrors.push(`unknown frontmatter field \"${key}\"`);\n\t\t}\n\t}\n\treturn errors;\n}\n\nfunction loadSkillFromFile(filePath: string, source: string): { skill: Skill | null; diagnostics: SkillDiagnostic[] } {\n\tconst diagnostics: SkillDiagnostic[] = [];\n\n\ttry {\n\t\tconst rawContent = readFileSync(filePath, \"utf-8\");\n\t\tconst { frontmatter } = parseFrontmatter<SkillFrontmatter>(rawContent);\n\t\tconst allKeys = Object.keys(frontmatter);\n\t\tconst skillDir = dirname(filePath);\n\t\tconst parentDirName = basename(skillDir);\n\n\t\tconst fieldErrors = validateFrontmatterFields(allKeys);\n\t\tfor (const error of fieldErrors) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: error, path: filePath });\n\t\t}\n\n\t\tconst descErrors = validateDescription(frontmatter.description);\n\t\tfor (const error of descErrors) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: error, path: filePath });\n\t\t}\n\n\t\tconst name = frontmatter.name || parentDirName;\n\n\t\tconst nameErrors = validateName(name, parentDirName);\n\t\tfor (const error of nameErrors) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: error, path: filePath });\n\t\t}\n\n\t\tif (!frontmatter.description || frontmatter.description.trim() === \"\") {\n\t\t\treturn { skill: null, diagnostics };\n\t\t}\n\n\t\treturn {\n\t\t\tskill: {\n\t\t\t\tname,\n\t\t\t\tdescription: frontmatter.description,\n\t\t\t\tfilePath,\n\t\t\t\tbaseDir: skillDir,\n\t\t\t\tsource,\n\t\t\t\tdisableModelInvocation: frontmatter[\"disable-model-invocation\"] === true,\n\t\t\t},\n\t\t\tdiagnostics,\n\t\t};\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : \"failed to parse skill file\";\n\t\tdiagnostics.push({ type: \"warning\", message, path: filePath });\n\t\treturn { skill: null, diagnostics };\n\t}\n}\n\nfunction loadSkillsFromDirInternal(dir: string, source: string, includeRootFiles: boolean): LoadSkillsResult {\n\tconst skills: Skill[] = [];\n\tconst diagnostics: SkillDiagnostic[] = [];\n\n\tif (!existsSync(dir)) {\n\t\treturn { skills, diagnostics };\n\t}\n\n\ttry {\n\t\tconst entries = readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.name.startsWith(\".\") || entry.name === \"node_modules\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst fullPath = join(dir, entry.name);\n\n\t\t\tlet isDirectory = entry.isDirectory();\n\t\t\tlet isFile = entry.isFile();\n\t\t\tif (entry.isSymbolicLink()) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(fullPath);\n\t\t\t\t\tisDirectory = stats.isDirectory();\n\t\t\t\t\tisFile = stats.isFile();\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isDirectory) {\n\t\t\t\tconst subResult = loadSkillsFromDirInternal(fullPath, source, false);\n\t\t\t\tskills.push(...subResult.skills);\n\t\t\t\tdiagnostics.push(...subResult.diagnostics);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif (!isFile) continue;\n\n\t\t\tconst isRootMd = includeRootFiles && entry.name.endsWith(\".md\");\n\t\t\tconst isSkillMd = !includeRootFiles && entry.name === \"SKILL.md\";\n\t\t\tif (!isRootMd && !isSkillMd) continue;\n\n\t\t\tconst result = loadSkillFromFile(fullPath, source);\n\t\t\tif (result.skill) {\n\t\t\t\tskills.push(result.skill);\n\t\t\t}\n\t\t\tdiagnostics.push(...result.diagnostics);\n\t\t}\n\t} catch {}\n\n\treturn { skills, diagnostics };\n}\n\nexport interface LoadSkillsOptions {\n\t/** Working directory for project-local skills. Default: process.cwd() */\n\tcwd?: string;\n\t/** Agent config directory for global skills. Default: ~/.pi/agent */\n\tagentDir?: string;\n\t/** Explicit skill paths (files or directories) */\n\tskillPaths?: string[];\n\t/** Include default skill directories. Default: true */\n\tincludeDefaults?: boolean;\n}\n\nfunction getDefaultAgentDir(): string {\n\tconst envDir = process.env.PI_CODING_AGENT_DIR;\n\tif (envDir) {\n\t\tif (envDir === \"~\") return homedir();\n\t\tif (envDir.startsWith(\"~/\")) return homedir() + envDir.slice(1);\n\t\treturn envDir;\n\t}\n\treturn join(homedir(), CONFIG_DIR_NAME, \"agent\");\n}\n\nfunction normalizePath(input: string): string {\n\tconst trimmed = input.trim();\n\tif (trimmed === \"~\") return homedir();\n\tif (trimmed.startsWith(\"~/\")) return join(homedir(), trimmed.slice(2));\n\tif (trimmed.startsWith(\"~\")) return join(homedir(), trimmed.slice(1));\n\treturn trimmed;\n}\n\nfunction resolveSkillPath(p: string, cwd: string): string {\n\tconst normalized = normalizePath(p);\n\treturn isAbsolute(normalized) ? normalized : resolve(cwd, normalized);\n}\n\n/**\n * Load skills from all configured locations.\n */\nexport function loadSkills(options: LoadSkillsOptions = {}): LoadSkillsResult {\n\tconst { cwd = process.cwd(), agentDir, skillPaths = [], includeDefaults = true } = options;\n\tconst resolvedAgentDir = agentDir ?? getDefaultAgentDir();\n\n\tconst skillMap = new Map<string, Skill>();\n\tconst realPathSet = new Set<string>();\n\tconst allDiagnostics: SkillDiagnostic[] = [];\n\tconst collisionDiagnostics: SkillDiagnostic[] = [];\n\n\tfunction addSkills(result: LoadSkillsResult) {\n\t\tallDiagnostics.push(...result.diagnostics);\n\t\tfor (const skill of result.skills) {\n\t\t\tlet realPath: string;\n\t\t\ttry {\n\t\t\t\trealPath = realpathSync(skill.filePath);\n\t\t\t} catch {\n\t\t\t\trealPath = skill.filePath;\n\t\t\t}\n\n\t\t\tif (realPathSet.has(realPath)) continue;\n\n\t\t\tconst existing = skillMap.get(skill.name);\n\t\t\tif (existing) {\n\t\t\t\tcollisionDiagnostics.push({\n\t\t\t\t\ttype: \"collision\",\n\t\t\t\t\tmessage: `name \"${skill.name}\" collision`,\n\t\t\t\t\tpath: skill.filePath,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tskillMap.set(skill.name, skill);\n\t\t\t\trealPathSet.add(realPath);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (includeDefaults) {\n\t\taddSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, \"skills\"), \"user\", true));\n\t\taddSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, \"skills\"), \"project\", true));\n\t}\n\n\tconst userSkillsDir = join(resolvedAgentDir, \"skills\");\n\tconst projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, \"skills\");\n\n\tconst isUnderPath = (target: string, root: string): boolean => {\n\t\tconst normalizedRoot = resolve(root);\n\t\tif (target === normalizedRoot) return true;\n\t\tconst prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;\n\t\treturn target.startsWith(prefix);\n\t};\n\n\tconst getSource = (resolvedPath: string): \"user\" | \"project\" | \"path\" => {\n\t\tif (!includeDefaults) {\n\t\t\tif (isUnderPath(resolvedPath, userSkillsDir)) return \"user\";\n\t\t\tif (isUnderPath(resolvedPath, projectSkillsDir)) return \"project\";\n\t\t}\n\t\treturn \"path\";\n\t};\n\n\tfor (const rawPath of skillPaths) {\n\t\tconst resolvedPath = resolveSkillPath(rawPath, cwd);\n\t\tif (!existsSync(resolvedPath)) {\n\t\t\tallDiagnostics.push({ type: \"warning\", message: \"skill path does not exist\", path: resolvedPath });\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(resolvedPath);\n\t\t\tconst source = getSource(resolvedPath);\n\t\t\tif (stats.isDirectory()) {\n\t\t\t\taddSkills(loadSkillsFromDirInternal(resolvedPath, source, true));\n\t\t\t} else if (stats.isFile() && resolvedPath.endsWith(\".md\")) {\n\t\t\t\tconst result = loadSkillFromFile(resolvedPath, source);\n\t\t\t\tif (result.skill) {\n\t\t\t\t\taddSkills({ skills: [result.skill], diagnostics: result.diagnostics });\n\t\t\t\t} else {\n\t\t\t\t\tallDiagnostics.push(...result.diagnostics);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tallDiagnostics.push({ type: \"warning\", message: \"skill path is not a markdown file\", path: resolvedPath });\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconst message = error instanceof Error ? error.message : \"failed to read skill path\";\n\t\t\tallDiagnostics.push({ type: \"warning\", message, path: resolvedPath });\n\t\t}\n\t}\n\n\treturn {\n\t\tskills: Array.from(skillMap.values()),\n\t\tdiagnostics: [...allDiagnostics, ...collisionDiagnostics],\n\t};\n}\n\n/**\n * Format skills for inclusion in a system prompt.\n * Uses XML format per Agent Skills standard.\n *\n * Skills with disableModelInvocation=true are excluded from the prompt.\n */\nexport function formatSkillsForPrompt(skills: Skill[]): string {\n\tconst visibleSkills = skills.filter((s) => !s.disableModelInvocation);\n\n\tif (visibleSkills.length === 0) {\n\t\treturn \"\";\n\t}\n\n\tconst lines = [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t];\n\n\tfor (const skill of visibleSkills) {\n\t\tlines.push(\" <skill>\");\n\t\tlines.push(` <name>${escapeXml(skill.name)}</name>`);\n\t\tlines.push(` <description>${escapeXml(skill.description)}</description>`);\n\t\tlines.push(` <location>${escapeXml(skill.filePath)}</location>`);\n\t\tlines.push(\" </skill>\");\n\t}\n\n\tlines.push(\"</available_skills>\");\n\n\treturn lines.join(\"\\n\");\n}\n\nfunction escapeXml(str: string): string {\n\treturn str\n\t\t.replace(/&/g, \"&amp;\")\n\t\t.replace(/</g, \"&lt;\")\n\t\t.replace(/>/g, \"&gt;\")\n\t\t.replace(/\"/g, \"&quot;\")\n\t\t.replace(/'/g, \"&apos;\");\n}\n"]}
package/dist/skills.js ADDED
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Skill loading and formatting for the system prompt.
3
+ * Implements the Agent Skills spec: https://agentskills.io/specification
4
+ *
5
+ * Standalone implementation - no dependency on the old SDK.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
10
+ import { parseFrontmatter } from "./utils/frontmatter.js";
11
+ const CONFIG_DIR_NAME = ".pi";
12
+ const ALLOWED_FRONTMATTER_FIELDS = new Set([
13
+ "name",
14
+ "description",
15
+ "license",
16
+ "compatibility",
17
+ "metadata",
18
+ "allowed-tools",
19
+ "disable-model-invocation",
20
+ ]);
21
+ const MAX_NAME_LENGTH = 64;
22
+ const MAX_DESCRIPTION_LENGTH = 1024;
23
+ function validateName(name, parentDirName) {
24
+ const errors = [];
25
+ if (name !== parentDirName) {
26
+ errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
27
+ }
28
+ if (name.length > MAX_NAME_LENGTH) {
29
+ errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);
30
+ }
31
+ if (!/^[a-z0-9-]+$/.test(name)) {
32
+ errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`);
33
+ }
34
+ if (name.startsWith("-") || name.endsWith("-")) {
35
+ errors.push(`name must not start or end with a hyphen`);
36
+ }
37
+ if (name.includes("--")) {
38
+ errors.push(`name must not contain consecutive hyphens`);
39
+ }
40
+ return errors;
41
+ }
42
+ function validateDescription(description) {
43
+ const errors = [];
44
+ if (!description || description.trim() === "") {
45
+ errors.push("description is required");
46
+ }
47
+ else if (description.length > MAX_DESCRIPTION_LENGTH) {
48
+ errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`);
49
+ }
50
+ return errors;
51
+ }
52
+ function validateFrontmatterFields(keys) {
53
+ const errors = [];
54
+ for (const key of keys) {
55
+ if (!ALLOWED_FRONTMATTER_FIELDS.has(key)) {
56
+ errors.push(`unknown frontmatter field "${key}"`);
57
+ }
58
+ }
59
+ return errors;
60
+ }
61
+ function loadSkillFromFile(filePath, source) {
62
+ const diagnostics = [];
63
+ try {
64
+ const rawContent = readFileSync(filePath, "utf-8");
65
+ const { frontmatter } = parseFrontmatter(rawContent);
66
+ const allKeys = Object.keys(frontmatter);
67
+ const skillDir = dirname(filePath);
68
+ const parentDirName = basename(skillDir);
69
+ const fieldErrors = validateFrontmatterFields(allKeys);
70
+ for (const error of fieldErrors) {
71
+ diagnostics.push({ type: "warning", message: error, path: filePath });
72
+ }
73
+ const descErrors = validateDescription(frontmatter.description);
74
+ for (const error of descErrors) {
75
+ diagnostics.push({ type: "warning", message: error, path: filePath });
76
+ }
77
+ const name = frontmatter.name || parentDirName;
78
+ const nameErrors = validateName(name, parentDirName);
79
+ for (const error of nameErrors) {
80
+ diagnostics.push({ type: "warning", message: error, path: filePath });
81
+ }
82
+ if (!frontmatter.description || frontmatter.description.trim() === "") {
83
+ return { skill: null, diagnostics };
84
+ }
85
+ return {
86
+ skill: {
87
+ name,
88
+ description: frontmatter.description,
89
+ filePath,
90
+ baseDir: skillDir,
91
+ source,
92
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true,
93
+ },
94
+ diagnostics,
95
+ };
96
+ }
97
+ catch (error) {
98
+ const message = error instanceof Error ? error.message : "failed to parse skill file";
99
+ diagnostics.push({ type: "warning", message, path: filePath });
100
+ return { skill: null, diagnostics };
101
+ }
102
+ }
103
+ function loadSkillsFromDirInternal(dir, source, includeRootFiles) {
104
+ const skills = [];
105
+ const diagnostics = [];
106
+ if (!existsSync(dir)) {
107
+ return { skills, diagnostics };
108
+ }
109
+ try {
110
+ const entries = readdirSync(dir, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
113
+ continue;
114
+ }
115
+ const fullPath = join(dir, entry.name);
116
+ let isDirectory = entry.isDirectory();
117
+ let isFile = entry.isFile();
118
+ if (entry.isSymbolicLink()) {
119
+ try {
120
+ const stats = statSync(fullPath);
121
+ isDirectory = stats.isDirectory();
122
+ isFile = stats.isFile();
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ }
128
+ if (isDirectory) {
129
+ const subResult = loadSkillsFromDirInternal(fullPath, source, false);
130
+ skills.push(...subResult.skills);
131
+ diagnostics.push(...subResult.diagnostics);
132
+ continue;
133
+ }
134
+ if (!isFile)
135
+ continue;
136
+ const isRootMd = includeRootFiles && entry.name.endsWith(".md");
137
+ const isSkillMd = !includeRootFiles && entry.name === "SKILL.md";
138
+ if (!isRootMd && !isSkillMd)
139
+ continue;
140
+ const result = loadSkillFromFile(fullPath, source);
141
+ if (result.skill) {
142
+ skills.push(result.skill);
143
+ }
144
+ diagnostics.push(...result.diagnostics);
145
+ }
146
+ }
147
+ catch { }
148
+ return { skills, diagnostics };
149
+ }
150
+ function getDefaultAgentDir() {
151
+ const envDir = process.env.PI_CODING_AGENT_DIR;
152
+ if (envDir) {
153
+ if (envDir === "~")
154
+ return homedir();
155
+ if (envDir.startsWith("~/"))
156
+ return homedir() + envDir.slice(1);
157
+ return envDir;
158
+ }
159
+ return join(homedir(), CONFIG_DIR_NAME, "agent");
160
+ }
161
+ function normalizePath(input) {
162
+ const trimmed = input.trim();
163
+ if (trimmed === "~")
164
+ return homedir();
165
+ if (trimmed.startsWith("~/"))
166
+ return join(homedir(), trimmed.slice(2));
167
+ if (trimmed.startsWith("~"))
168
+ return join(homedir(), trimmed.slice(1));
169
+ return trimmed;
170
+ }
171
+ function resolveSkillPath(p, cwd) {
172
+ const normalized = normalizePath(p);
173
+ return isAbsolute(normalized) ? normalized : resolve(cwd, normalized);
174
+ }
175
+ /**
176
+ * Load skills from all configured locations.
177
+ */
178
+ export function loadSkills(options = {}) {
179
+ const { cwd = process.cwd(), agentDir, skillPaths = [], includeDefaults = true } = options;
180
+ const resolvedAgentDir = agentDir ?? getDefaultAgentDir();
181
+ const skillMap = new Map();
182
+ const realPathSet = new Set();
183
+ const allDiagnostics = [];
184
+ const collisionDiagnostics = [];
185
+ function addSkills(result) {
186
+ allDiagnostics.push(...result.diagnostics);
187
+ for (const skill of result.skills) {
188
+ let realPath;
189
+ try {
190
+ realPath = realpathSync(skill.filePath);
191
+ }
192
+ catch {
193
+ realPath = skill.filePath;
194
+ }
195
+ if (realPathSet.has(realPath))
196
+ continue;
197
+ const existing = skillMap.get(skill.name);
198
+ if (existing) {
199
+ collisionDiagnostics.push({
200
+ type: "collision",
201
+ message: `name "${skill.name}" collision`,
202
+ path: skill.filePath,
203
+ });
204
+ }
205
+ else {
206
+ skillMap.set(skill.name, skill);
207
+ realPathSet.add(realPath);
208
+ }
209
+ }
210
+ }
211
+ if (includeDefaults) {
212
+ addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true));
213
+ addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", true));
214
+ }
215
+ const userSkillsDir = join(resolvedAgentDir, "skills");
216
+ const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills");
217
+ const isUnderPath = (target, root) => {
218
+ const normalizedRoot = resolve(root);
219
+ if (target === normalizedRoot)
220
+ return true;
221
+ const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`;
222
+ return target.startsWith(prefix);
223
+ };
224
+ const getSource = (resolvedPath) => {
225
+ if (!includeDefaults) {
226
+ if (isUnderPath(resolvedPath, userSkillsDir))
227
+ return "user";
228
+ if (isUnderPath(resolvedPath, projectSkillsDir))
229
+ return "project";
230
+ }
231
+ return "path";
232
+ };
233
+ for (const rawPath of skillPaths) {
234
+ const resolvedPath = resolveSkillPath(rawPath, cwd);
235
+ if (!existsSync(resolvedPath)) {
236
+ allDiagnostics.push({ type: "warning", message: "skill path does not exist", path: resolvedPath });
237
+ continue;
238
+ }
239
+ try {
240
+ const stats = statSync(resolvedPath);
241
+ const source = getSource(resolvedPath);
242
+ if (stats.isDirectory()) {
243
+ addSkills(loadSkillsFromDirInternal(resolvedPath, source, true));
244
+ }
245
+ else if (stats.isFile() && resolvedPath.endsWith(".md")) {
246
+ const result = loadSkillFromFile(resolvedPath, source);
247
+ if (result.skill) {
248
+ addSkills({ skills: [result.skill], diagnostics: result.diagnostics });
249
+ }
250
+ else {
251
+ allDiagnostics.push(...result.diagnostics);
252
+ }
253
+ }
254
+ else {
255
+ allDiagnostics.push({ type: "warning", message: "skill path is not a markdown file", path: resolvedPath });
256
+ }
257
+ }
258
+ catch (error) {
259
+ const message = error instanceof Error ? error.message : "failed to read skill path";
260
+ allDiagnostics.push({ type: "warning", message, path: resolvedPath });
261
+ }
262
+ }
263
+ return {
264
+ skills: Array.from(skillMap.values()),
265
+ diagnostics: [...allDiagnostics, ...collisionDiagnostics],
266
+ };
267
+ }
268
+ /**
269
+ * Format skills for inclusion in a system prompt.
270
+ * Uses XML format per Agent Skills standard.
271
+ *
272
+ * Skills with disableModelInvocation=true are excluded from the prompt.
273
+ */
274
+ export function formatSkillsForPrompt(skills) {
275
+ const visibleSkills = skills.filter((s) => !s.disableModelInvocation);
276
+ if (visibleSkills.length === 0) {
277
+ return "";
278
+ }
279
+ const lines = [
280
+ "\n\nThe following skills provide specialized instructions for specific tasks.",
281
+ "Use the read tool to load a skill's file when the task matches its description.",
282
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
283
+ "",
284
+ "<available_skills>",
285
+ ];
286
+ for (const skill of visibleSkills) {
287
+ lines.push(" <skill>");
288
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
289
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
290
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
291
+ lines.push(" </skill>");
292
+ }
293
+ lines.push("</available_skills>");
294
+ return lines.join("\n");
295
+ }
296
+ function escapeXml(str) {
297
+ return str
298
+ .replace(/&/g, "&amp;")
299
+ .replace(/</g, "&lt;")
300
+ .replace(/>/g, "&gt;")
301
+ .replace(/"/g, "&quot;")
302
+ .replace(/'/g, "&apos;");
303
+ }
304
+ //# sourceMappingURL=skills.js.map