@poping/yome 0.0.2 → 0.0.3

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 (185) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +11 -0
  3. package/README.md +308 -27
  4. package/README.zh-CN.md +335 -0
  5. package/dist/agent.d.ts +24 -2
  6. package/dist/agent.js +34 -2
  7. package/dist/agent.js.map +1 -1
  8. package/dist/context.d.ts +2 -0
  9. package/dist/context.js +116 -11
  10. package/dist/context.js.map +1 -1
  11. package/dist/index.js +147 -9
  12. package/dist/index.js.map +1 -1
  13. package/dist/llm.js +45 -2
  14. package/dist/llm.js.map +1 -1
  15. package/dist/loops/chain.js +8 -0
  16. package/dist/loops/chain.js.map +1 -1
  17. package/dist/loops/evaluator.js +8 -0
  18. package/dist/loops/evaluator.js.map +1 -1
  19. package/dist/loops/orchestrator.js +8 -0
  20. package/dist/loops/orchestrator.js.map +1 -1
  21. package/dist/loops/parallel.js.map +1 -1
  22. package/dist/loops/route.js +8 -0
  23. package/dist/loops/route.js.map +1 -1
  24. package/dist/loops/simple.js +15 -0
  25. package/dist/loops/simple.js.map +1 -1
  26. package/dist/permissions/index.d.ts +1 -1
  27. package/dist/permissions/index.js +1 -1
  28. package/dist/permissions/index.js.map +1 -1
  29. package/dist/permissions/loader.d.ts +20 -1
  30. package/dist/permissions/loader.js +51 -0
  31. package/dist/permissions/loader.js.map +1 -1
  32. package/dist/redact.d.ts +56 -0
  33. package/dist/redact.js +191 -0
  34. package/dist/redact.js.map +1 -0
  35. package/dist/skills/runner/applescript.d.ts +49 -0
  36. package/dist/skills/runner/applescript.js +100 -0
  37. package/dist/skills/runner/applescript.js.map +1 -0
  38. package/dist/skills/runner/dispatcher.d.ts +57 -0
  39. package/dist/skills/runner/dispatcher.js +407 -0
  40. package/dist/skills/runner/dispatcher.js.map +1 -0
  41. package/dist/skills/runner/kernel.d.ts +8 -0
  42. package/dist/skills/runner/kernel.js +732 -0
  43. package/dist/skills/runner/kernel.js.map +1 -0
  44. package/dist/skills/runner/tokenizer.d.ts +36 -0
  45. package/dist/skills/runner/tokenizer.js +177 -0
  46. package/dist/skills/runner/tokenizer.js.map +1 -0
  47. package/dist/threadCli.d.ts +11 -0
  48. package/dist/threadCli.js +177 -0
  49. package/dist/threadCli.js.map +1 -0
  50. package/dist/threadShare.d.ts +21 -0
  51. package/dist/threadShare.js +121 -0
  52. package/dist/threadShare.js.map +1 -0
  53. package/dist/threadSubmit.d.ts +32 -0
  54. package/dist/threadSubmit.js +199 -0
  55. package/dist/threadSubmit.js.map +1 -0
  56. package/dist/tools/bash.js +68 -13
  57. package/dist/tools/bash.js.map +1 -1
  58. package/dist/tools/index.d.ts +22 -2
  59. package/dist/tools/index.js +41 -5
  60. package/dist/tools/index.js.map +1 -1
  61. package/dist/tools/skillCall.d.ts +2 -0
  62. package/dist/tools/skillCall.js +77 -0
  63. package/dist/tools/skillCall.js.map +1 -0
  64. package/dist/ui/AgentPicker.js +3 -3
  65. package/dist/ui/AgentPicker.js.map +1 -1
  66. package/dist/ui/App.js +214 -61
  67. package/dist/ui/App.js.map +1 -1
  68. package/dist/ui/Banner.d.ts +2 -1
  69. package/dist/ui/Banner.js +23 -4
  70. package/dist/ui/Banner.js.map +1 -1
  71. package/dist/ui/InputBar.js +6 -3
  72. package/dist/ui/InputBar.js.map +1 -1
  73. package/dist/ui/Markdown.d.ts +2 -2
  74. package/dist/ui/Markdown.js +22 -7
  75. package/dist/ui/Markdown.js.map +1 -1
  76. package/dist/ui/MarketplacePicker.d.ts +7 -0
  77. package/dist/ui/MarketplacePicker.js +122 -0
  78. package/dist/ui/MarketplacePicker.js.map +1 -0
  79. package/dist/ui/MessageList.d.ts +4 -1
  80. package/dist/ui/MessageList.js +72 -7
  81. package/dist/ui/MessageList.js.map +1 -1
  82. package/dist/ui/ModelPicker.js +4 -4
  83. package/dist/ui/ModelPicker.js.map +1 -1
  84. package/dist/ui/PermissionPrompt.d.ts +16 -4
  85. package/dist/ui/PermissionPrompt.js +60 -15
  86. package/dist/ui/PermissionPrompt.js.map +1 -1
  87. package/dist/ui/SessionPicker.js +2 -2
  88. package/dist/ui/SessionPicker.js.map +1 -1
  89. package/dist/ui/ShimmerText.d.ts +8 -0
  90. package/dist/ui/ShimmerText.js +40 -0
  91. package/dist/ui/ShimmerText.js.map +1 -0
  92. package/dist/ui/Spinner.js +3 -9
  93. package/dist/ui/Spinner.js.map +1 -1
  94. package/dist/ui/TogglePicker.js +4 -4
  95. package/dist/ui/TogglePicker.js.map +1 -1
  96. package/dist/ui/ToolResult.js +6 -0
  97. package/dist/ui/ToolResult.js.map +1 -1
  98. package/dist/ui/UnifiedSkillsPicker.d.ts +10 -0
  99. package/dist/ui/UnifiedSkillsPicker.js +63 -0
  100. package/dist/ui/UnifiedSkillsPicker.js.map +1 -0
  101. package/dist/ui/animation.d.ts +3 -0
  102. package/dist/ui/animation.js +48 -0
  103. package/dist/ui/animation.js.map +1 -0
  104. package/dist/ui/useThrottledStream.d.ts +7 -0
  105. package/dist/ui/useThrottledStream.js +63 -0
  106. package/dist/ui/useThrottledStream.js.map +1 -0
  107. package/dist/yomeSkills/auth.d.ts +20 -0
  108. package/dist/yomeSkills/auth.js +70 -0
  109. package/dist/yomeSkills/auth.js.map +1 -0
  110. package/dist/yomeSkills/blacklist.d.ts +33 -0
  111. package/dist/yomeSkills/blacklist.js +101 -0
  112. package/dist/yomeSkills/blacklist.js.map +1 -0
  113. package/dist/yomeSkills/capabilities.d.ts +54 -0
  114. package/dist/yomeSkills/capabilities.js +175 -0
  115. package/dist/yomeSkills/capabilities.js.map +1 -0
  116. package/dist/yomeSkills/capabilityGuard.d.ts +31 -0
  117. package/dist/yomeSkills/capabilityGuard.js +113 -0
  118. package/dist/yomeSkills/capabilityGuard.js.map +1 -0
  119. package/dist/yomeSkills/cli.d.ts +17 -0
  120. package/dist/yomeSkills/cli.js +477 -0
  121. package/dist/yomeSkills/cli.js.map +1 -0
  122. package/dist/yomeSkills/devLink.d.ts +17 -0
  123. package/dist/yomeSkills/devLink.js +91 -0
  124. package/dist/yomeSkills/devLink.js.map +1 -0
  125. package/dist/yomeSkills/doctor.d.ts +13 -0
  126. package/dist/yomeSkills/doctor.js +152 -0
  127. package/dist/yomeSkills/doctor.js.map +1 -0
  128. package/dist/yomeSkills/enable.d.ts +8 -0
  129. package/dist/yomeSkills/enable.js +67 -0
  130. package/dist/yomeSkills/enable.js.map +1 -0
  131. package/dist/yomeSkills/hubPing.d.ts +1 -0
  132. package/dist/yomeSkills/hubPing.js +41 -0
  133. package/dist/yomeSkills/hubPing.js.map +1 -0
  134. package/dist/yomeSkills/install.d.ts +18 -0
  135. package/dist/yomeSkills/install.js +143 -0
  136. package/dist/yomeSkills/install.js.map +1 -0
  137. package/dist/yomeSkills/installGithub.d.ts +26 -0
  138. package/dist/yomeSkills/installGithub.js +192 -0
  139. package/dist/yomeSkills/installGithub.js.map +1 -0
  140. package/dist/yomeSkills/installMeta.d.ts +8 -0
  141. package/dist/yomeSkills/installMeta.js +76 -0
  142. package/dist/yomeSkills/installMeta.js.map +1 -0
  143. package/dist/yomeSkills/integrity.d.ts +26 -0
  144. package/dist/yomeSkills/integrity.js +107 -0
  145. package/dist/yomeSkills/integrity.js.map +1 -0
  146. package/dist/yomeSkills/invoke.d.ts +29 -0
  147. package/dist/yomeSkills/invoke.js +103 -0
  148. package/dist/yomeSkills/invoke.js.map +1 -0
  149. package/dist/yomeSkills/list.d.ts +11 -0
  150. package/dist/yomeSkills/list.js +55 -0
  151. package/dist/yomeSkills/list.js.map +1 -0
  152. package/dist/yomeSkills/login.d.ts +41 -0
  153. package/dist/yomeSkills/login.js +221 -0
  154. package/dist/yomeSkills/login.js.map +1 -0
  155. package/dist/yomeSkills/manifest.d.ts +60 -0
  156. package/dist/yomeSkills/manifest.js +47 -0
  157. package/dist/yomeSkills/manifest.js.map +1 -0
  158. package/dist/yomeSkills/paths.d.ts +13 -0
  159. package/dist/yomeSkills/paths.js +33 -0
  160. package/dist/yomeSkills/paths.js.map +1 -0
  161. package/dist/yomeSkills/publish.d.ts +16 -0
  162. package/dist/yomeSkills/publish.js +109 -0
  163. package/dist/yomeSkills/publish.js.map +1 -0
  164. package/dist/yomeSkills/rollback.d.ts +10 -0
  165. package/dist/yomeSkills/rollback.js +83 -0
  166. package/dist/yomeSkills/rollback.js.map +1 -0
  167. package/dist/yomeSkills/search.d.ts +21 -0
  168. package/dist/yomeSkills/search.js +31 -0
  169. package/dist/yomeSkills/search.js.map +1 -0
  170. package/dist/yomeSkills/skillsIndex.d.ts +36 -0
  171. package/dist/yomeSkills/skillsIndex.js +111 -0
  172. package/dist/yomeSkills/skillsIndex.js.map +1 -0
  173. package/dist/yomeSkills/unified.d.ts +53 -0
  174. package/dist/yomeSkills/unified.js +187 -0
  175. package/dist/yomeSkills/unified.js.map +1 -0
  176. package/dist/yomeSkills/uninstall.d.ts +7 -0
  177. package/dist/yomeSkills/uninstall.js +22 -0
  178. package/dist/yomeSkills/uninstall.js.map +1 -0
  179. package/dist/yomeSkills/update.d.ts +18 -0
  180. package/dist/yomeSkills/update.js +75 -0
  181. package/dist/yomeSkills/update.js.map +1 -0
  182. package/dist/yomeSkills/validate.d.ts +11 -0
  183. package/dist/yomeSkills/validate.js +99 -0
  184. package/dist/yomeSkills/validate.js.map +1 -0
  185. package/package.json +14 -5
@@ -0,0 +1,732 @@
1
+ // cli/src/skills/runner/kernel.ts
2
+ //
3
+ // Yome agentic bash kernel.
4
+ //
5
+ // The LLM in cli only ever sees one execution tool: `Bash`. To let it
6
+ // invoke installed hub skills with the same ergonomic command-line shape
7
+ // a human would type (`ppt new ~/Desktop/x.pptx`), the Bash tool calls
8
+ // `tryKernel` BEFORE handing anything to /bin/sh. The kernel:
9
+ //
10
+ // 1. Tokenises the line (quote-aware; no shell expansion).
11
+ // 2. Refuses to handle compound lines (pipes, &&, ;, redirects, subshell)
12
+ // so genuine shell stays as shell.
13
+ // 3. Refuses to handle reserved tokens (git, ls, cd, ...) — those are
14
+ // always real shell, never skill invocations.
15
+ // 4. If tokens[0] matches an installed hub skill's `domain`, runs the
16
+ // action via invokeSkill — same path the macOS app uses, including
17
+ // capability gating.
18
+ // 5. Supports `--help` at three levels (global / domain / action) and
19
+ // renders compact TSV-ish text the LLM can scan cheaply.
20
+ //
21
+ // Anything the kernel doesn't claim falls through to plain shell.
22
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { tokenize, looksCompound, splitOnUnquotedAmpAmp } from './tokenizer.js';
25
+ import { getInstalledFast } from '../../yomeSkills/skillsIndex.js';
26
+ import { readManifest } from '../../yomeSkills/manifest.js';
27
+ import { invokeSkill } from '../../yomeSkills/invoke.js';
28
+ import { loadMacosBackend } from './dispatcher.js';
29
+ // Anything that's a system command we actively want to stay as a real
30
+ // shell call. We refuse to interpret these as a skill domain even if a
31
+ // future user installs a same-named skill — safety first.
32
+ const RESERVED_SYSTEM_COMMANDS = new Set([
33
+ // Coreutils + common
34
+ 'ls', 'cd', 'pwd', 'cp', 'mv', 'rm', 'mkdir', 'rmdir', 'touch',
35
+ 'cat', 'head', 'tail', 'less', 'more', 'grep', 'find', 'sort', 'uniq', 'wc',
36
+ 'echo', 'printf', 'true', 'false', 'test', 'tr', 'sed', 'awk', 'cut',
37
+ // Shells
38
+ 'sh', 'bash', 'zsh', 'fish', 'env',
39
+ // Tooling
40
+ 'git', 'gh', 'curl', 'wget', 'ssh', 'scp', 'rsync', 'open', 'osascript',
41
+ 'node', 'npm', 'pnpm', 'yarn', 'npx', 'tsc',
42
+ 'python', 'python3', 'pip', 'pip3', 'ruby', 'go', 'cargo', 'rustc',
43
+ 'java', 'javac', 'mvn', 'gradle',
44
+ 'docker', 'kubectl', 'make',
45
+ // Yome itself — `yome <subcmd>` always means the cli, never a skill domain
46
+ 'yome',
47
+ ]);
48
+ // ── Public entry ────────────────────────────────────────────────────
49
+ export async function tryKernel(commandLine) {
50
+ const trimmed = commandLine.trim();
51
+ if (!trimmed)
52
+ return notHandled();
53
+ // ── Batch mode (must come BEFORE looksCompound — heredoc uses `<<`) ──
54
+ // Two accepted shapes:
55
+ // <domain> batch <<EOF\n...\nEOF
56
+ // <domain> batch\n<cmd>\n<cmd>...
57
+ // Both are claimed by the kernel even when the closing tag is missing
58
+ // so we can return a useful error instead of silently passing the
59
+ // heredoc to /bin/sh and letting the user wonder what happened.
60
+ //
61
+ // Carve out `<domain> batch --help` first — that's a help request, not
62
+ // a batch invocation with an empty body.
63
+ const helpSnif = trimmed.match(/^(\w+)\s+batch\b\s+(--help|-h)\b\s*$/);
64
+ if (helpSnif) {
65
+ const installedHelp = getInstalledFast().filter((s) => s.status === 'enabled');
66
+ const sk = installedHelp.find((s) => s.domain === helpSnif[1]);
67
+ if (sk)
68
+ return ok(renderBatchHelp(sk));
69
+ }
70
+ const batch = parseBatchCommand(trimmed);
71
+ if (batch)
72
+ return runBatch(batch);
73
+ // ── `xl X && xl Y && ...` chains ───────────────────────────────
74
+ // If a line is ONLY composed of hub-skill invocations joined by
75
+ // top-level `&&`, the kernel runs them sequentially with short-circuit
76
+ // semantics rather than handing the whole compound line to /bin/sh
77
+ // (which would just say "xl: command not found"). Anything that's
78
+ // not a pure skill chain (mixed shell + skill, presence of `|`/`;`/
79
+ // redirect/subshell, unknown domains, --help mid-chain) falls through
80
+ // to looksCompound below and out to real shell.
81
+ if (trimmed.includes('&&')) {
82
+ const chainResult = await tryRunSkillChain(trimmed);
83
+ if (chainResult)
84
+ return chainResult;
85
+ }
86
+ // Compound shell stays shell.
87
+ if (looksCompound(trimmed))
88
+ return notHandled();
89
+ const tokens = tokenize(trimmed);
90
+ if (tokens.length === 0)
91
+ return notHandled();
92
+ const first = tokens[0];
93
+ // Global help — only when the user explicitly typed something we own.
94
+ // We don't claim a bare `--help` because that might mean something else
95
+ // in the host shell.
96
+ if (first === 'yome-skills' && (tokens.length === 1 || tokens[1] === '--help')) {
97
+ return ok(renderGlobalHelp());
98
+ }
99
+ if (RESERVED_SYSTEM_COMMANDS.has(first))
100
+ return notHandled();
101
+ // Find the installed hub skill whose `domain` matches token[0].
102
+ // Domains are short strings declared in yome-skill.json (e.g. "ppt").
103
+ const installed = getInstalledFast().filter((s) => s.status === 'enabled');
104
+ const skill = installed.find((s) => s.domain === first);
105
+ if (!skill)
106
+ return notHandled();
107
+ // From here on we *own* the line. Help routes return ok() with help text;
108
+ // execution routes go through invokeSkill.
109
+ // ppt --help / ppt -h
110
+ if (tokens.length === 1 || tokens[1] === '--help' || tokens[1] === '-h') {
111
+ return ok(renderDomainHelp(skill));
112
+ }
113
+ // ppt --doc → list templates from skill repo's docs/
114
+ // ppt --doc <name> → read that specific template
115
+ if (tokens[1] === '--doc') {
116
+ if (tokens.length === 2)
117
+ return ok(renderDocList(skill));
118
+ return ok(renderDocOne(skill, tokens[2]));
119
+ }
120
+ const action = tokens[1];
121
+ // ppt batch --help — kernel-level meta action.
122
+ if (action === 'batch' && (tokens.includes('--help') || tokens.includes('-h'))) {
123
+ return ok(renderBatchHelp(skill));
124
+ }
125
+ // ppt <action> --help
126
+ if (tokens.includes('--help') || tokens.includes('-h')) {
127
+ return ok(renderActionHelp(skill, action));
128
+ }
129
+ // Parse argv: collect positionals + flags
130
+ const { positionals, flags, parseError } = parseArgs(tokens.slice(2));
131
+ if (parseError) {
132
+ return err(parseError + '\n\n' + renderActionHelp(skill, action), 2);
133
+ }
134
+ const r = await invokeSkill({
135
+ slugOrDomain: skill.slug,
136
+ action,
137
+ positionals,
138
+ flags,
139
+ });
140
+ return {
141
+ handled: true,
142
+ stdout: r.stdout,
143
+ stderr: r.stderr,
144
+ exitCode: r.exitCode,
145
+ };
146
+ }
147
+ // ── `<domain> X && <domain> Y && ...` chain runner ───────────────────
148
+ //
149
+ // Returns:
150
+ // - null → not a pure skill chain (caller should keep going / fall to shell)
151
+ // - KernelResult → kernel handled the whole chain
152
+ //
153
+ // Acceptance criteria for "pure skill chain":
154
+ // * line splits on top-level `&&` into >= 2 segments
155
+ // * every segment, after tokenize+trim, starts with a token that maps
156
+ // to an installed+enabled hub-skill domain
157
+ // * no segment is a meta-action we can't safely chain (batch/help/--doc) —
158
+ // those would interact with the chain in surprising ways, so we let
159
+ // real shell handle them (exits 127, but only if user really wrote
160
+ // `xl --help && xl batch ...` which is nonsensical anyway)
161
+ //
162
+ // Execution semantics: bash-like `&&` short-circuit. Stop on first
163
+ // non-zero exit; return that exit code. Stdout is the concatenation of
164
+ // each successful segment's stdout; stderr is the failing segment's
165
+ // stderr (mirroring `bash -c 'a && b && c'`).
166
+ async function tryRunSkillChain(line) {
167
+ const segments = splitOnUnquotedAmpAmp(line);
168
+ if (segments.length < 2)
169
+ return null;
170
+ const installed = getInstalledFast().filter((s) => s.status === 'enabled');
171
+ const domains = new Set(installed.map((s) => s.domain));
172
+ // Pre-flight: every segment must (a) tokenize to >= 1 token, (b) lead
173
+ // with a known skill domain, (c) NOT be a meta-action.
174
+ for (const seg of segments) {
175
+ if (looksCompound(seg))
176
+ return null; // segment itself has |/;/redirect
177
+ const toks = tokenize(seg);
178
+ if (toks.length === 0)
179
+ return null;
180
+ const head = toks[0];
181
+ if (RESERVED_SYSTEM_COMMANDS.has(head))
182
+ return null;
183
+ if (!domains.has(head))
184
+ return null;
185
+ // Refuse to chain meta-actions — they have non-standard exit semantics.
186
+ const sub = toks[1];
187
+ if (sub === 'batch' || sub === '--help' || sub === '-h' || sub === '--doc')
188
+ return null;
189
+ }
190
+ // All-skill chain confirmed. Execute sequentially, short-circuiting on
191
+ // first non-zero exit.
192
+ const stdoutParts = [];
193
+ let lastResult = { handled: true, stdout: '', stderr: '', exitCode: 0 };
194
+ for (const seg of segments) {
195
+ const r = await tryKernel(seg);
196
+ if (!r.handled) {
197
+ // Defensive — pre-flight should have caught this. Bail to shell.
198
+ return null;
199
+ }
200
+ if (r.stdout)
201
+ stdoutParts.push(r.stdout);
202
+ lastResult = r;
203
+ if (r.exitCode !== 0) {
204
+ return {
205
+ handled: true,
206
+ stdout: stdoutParts.join('\n'),
207
+ stderr: r.stderr,
208
+ exitCode: r.exitCode,
209
+ };
210
+ }
211
+ }
212
+ return {
213
+ handled: true,
214
+ stdout: stdoutParts.join('\n'),
215
+ stderr: lastResult.stderr,
216
+ exitCode: 0,
217
+ };
218
+ }
219
+ function parseArgs(rest) {
220
+ const positionals = [];
221
+ const flags = {};
222
+ for (let i = 0; i < rest.length; i++) {
223
+ const tok = rest[i];
224
+ // --flag=value
225
+ if (tok.startsWith('--')) {
226
+ const eq = tok.indexOf('=');
227
+ if (eq > 0) {
228
+ const key = tok.slice(2, eq);
229
+ flags[key] = tok.slice(eq + 1);
230
+ continue;
231
+ }
232
+ // --flag (bare). Either consume next non-flag token as value, or
233
+ // treat as boolean true.
234
+ const key = tok.slice(2);
235
+ const next = rest[i + 1];
236
+ if (next !== undefined && !next.startsWith('--')) {
237
+ flags[key] = next;
238
+ i++;
239
+ }
240
+ else {
241
+ flags[key] = true;
242
+ }
243
+ continue;
244
+ }
245
+ // Plain positional
246
+ positionals.push(tok);
247
+ }
248
+ return { positionals, flags };
249
+ }
250
+ // ── Help renderers ──────────────────────────────────────────────────
251
+ /**
252
+ * `yome-skills` (global) — list all installed hub skills.
253
+ */
254
+ function renderGlobalHelp() {
255
+ const all = getInstalledFast().filter((s) => s.status === 'enabled');
256
+ if (all.length === 0) {
257
+ return [
258
+ 'No hub skills installed.',
259
+ '',
260
+ 'Install one with:',
261
+ ' yome skill install github:<owner>/<repo>',
262
+ 'Browse the hub:',
263
+ ' https://yome.work/skills',
264
+ ].join('\n');
265
+ }
266
+ const lines = [];
267
+ lines.push('Installed hub skills (use `<domain> --help` for actions):');
268
+ lines.push('');
269
+ lines.push(['DOMAIN', 'SLUG', 'VERSION', 'DESCRIPTION'].join('\t'));
270
+ for (const s of all) {
271
+ lines.push([s.domain ?? '?', s.slug, s.version ?? '?', s.description ?? ''].join('\t'));
272
+ }
273
+ return lines.join('\n');
274
+ }
275
+ /**
276
+ * `<domain> --help` — Layer-2 signature documentation.
277
+ *
278
+ * Resolution order:
279
+ * 1. Skill repo's `SIGNATURE.md` (authored by the skill maintainer —
280
+ * this is what shows up to the LLM and should be hand-tuned for
281
+ * LLM consumption: one line per action, defaults inline, no
282
+ * hidden compression tricks).
283
+ * 2. Auto-generated from backends/macos/manifest.json args (so newly
284
+ * published skills always have *something* to show).
285
+ *
286
+ * We append a tiny footer about batch + --doc so the model knows those
287
+ * exist without the skill author having to remember.
288
+ */
289
+ function renderDomainHelp(skill) {
290
+ const sigPath = join(skill.installedAt, 'SIGNATURE.md');
291
+ let body;
292
+ if (existsSync(sigPath)) {
293
+ try {
294
+ body = readFileSync(sigPath, 'utf-8').trim();
295
+ }
296
+ catch {
297
+ body = renderAutoSignature(skill);
298
+ }
299
+ }
300
+ else {
301
+ body = renderAutoSignature(skill);
302
+ }
303
+ const footer = [
304
+ '',
305
+ `--- batch + docs ---`,
306
+ `${skill.domain} batch [--keep-going] [--merge] <<EOF\\n<cmd1>\\n<cmd2>\\nEOF`,
307
+ `${skill.domain} --doc list available templates / cookbooks`,
308
+ `${skill.domain} --doc <name> read one template`,
309
+ `${skill.domain} <action> --help per-action argument detail`,
310
+ ].join('\n');
311
+ return body + '\n' + footer;
312
+ }
313
+ /**
314
+ * Fallback when the skill repo doesn't ship a SIGNATURE.md. We render
315
+ * a usable signature from the backend manifest — verbose but always works.
316
+ * Skill authors are encouraged to hand-write SIGNATURE.md to override this.
317
+ */
318
+ function renderAutoSignature(skill) {
319
+ const manifest = readManifest(skill.installedAt);
320
+ const backend = loadMacosBackend(skill.installedAt);
321
+ const lines = [];
322
+ lines.push(`${skill.domain} — ${skill.name ?? skill.slug} (${skill.slug} v${skill.version ?? '?'})`);
323
+ if (skill.description)
324
+ lines.push(skill.description);
325
+ lines.push('');
326
+ // Build a one-line-per-action signature from the manifest args.
327
+ if (backend) {
328
+ for (const [action, spec] of Object.entries(backend.actions)) {
329
+ const positionals = [];
330
+ const flags = [];
331
+ for (const a of spec.args ?? []) {
332
+ const isPositional = a.from.split('|').some((s) => s.trim() === 'positional');
333
+ if (isPositional) {
334
+ positionals.push(a.required ? `<${a.name}>` : `[${a.name}]`);
335
+ }
336
+ else {
337
+ const flagName = a.from.split('|').map((s) => s.trim()).find((s) => s.startsWith('--')) ?? `--${a.name}`;
338
+ if (a.type === 'bool') {
339
+ flags.push(`[${flagName}]`);
340
+ }
341
+ else {
342
+ flags.push(a.required ? `${flagName}=<${a.name}>` : `[${flagName}=<${a.name}>]`);
343
+ }
344
+ }
345
+ }
346
+ const tail = [...positionals, ...flags].join(' ');
347
+ // Pull description from manifest.commands when available.
348
+ let desc = '';
349
+ if (manifest && Array.isArray(manifest.commands)) {
350
+ for (const c of manifest.commands) {
351
+ if (c.action === action && c.desc) {
352
+ desc = ` # ${c.desc}`;
353
+ break;
354
+ }
355
+ }
356
+ }
357
+ lines.push(` ${skill.domain} ${action}${tail ? ' ' + tail : ''}${desc}`);
358
+ }
359
+ }
360
+ else {
361
+ lines.push(' (no backend installed for this platform)');
362
+ }
363
+ return lines.join('\n');
364
+ }
365
+ /**
366
+ * `<domain> <action> --help` — list args for one action by reading the
367
+ * macOS backend manifest. Falls back to a generic message when there's
368
+ * no backend installed.
369
+ */
370
+ function renderActionHelp(skill, action) {
371
+ const backend = loadMacosBackend(skill.installedAt);
372
+ const manifest = readManifest(skill.installedAt);
373
+ // Pull the human-friendly description from manifest.commands when available.
374
+ let desc;
375
+ if (manifest && Array.isArray(manifest.commands)) {
376
+ for (const c of manifest.commands) {
377
+ if (c.action === action) {
378
+ desc = c.desc;
379
+ break;
380
+ }
381
+ }
382
+ }
383
+ if (!backend) {
384
+ return `${skill.domain} ${action} — no macOS backend installed for this skill.`;
385
+ }
386
+ const spec = backend.actions[action];
387
+ if (!spec) {
388
+ const known = Object.keys(backend.actions).join(', ');
389
+ return `Unknown action: ${skill.domain} ${action}\nAvailable: ${known}`;
390
+ }
391
+ const lines = [];
392
+ lines.push(`${skill.domain} ${action}${desc ? ' — ' + desc : ''}`);
393
+ lines.push('');
394
+ const positionalArgs = (spec.args ?? []).filter((a) => a.from.split('|').some((s) => s.trim() === 'positional'));
395
+ const flagArgs = (spec.args ?? []).filter((a) => a.from.split('|').some((s) => s.trim().startsWith('--')));
396
+ if (positionalArgs.length > 0) {
397
+ lines.push('POSITIONAL');
398
+ for (const a of positionalArgs) {
399
+ const req = a.required ? ' (required)' : '';
400
+ const def = a.default !== undefined && a.default !== '' && a.default !== false
401
+ ? ` [default: ${String(a.default)}]`
402
+ : '';
403
+ lines.push(` <${a.name}>${req}${def}`);
404
+ }
405
+ lines.push('');
406
+ }
407
+ if (flagArgs.length > 0) {
408
+ lines.push('FLAGS');
409
+ for (const a of flagArgs) {
410
+ const req = a.required ? ' (required)' : '';
411
+ const def = a.default !== undefined && a.default !== '' && a.default !== false
412
+ ? ` [default: ${String(a.default)}]`
413
+ : '';
414
+ const ty = a.type ? ` (${a.type})` : '';
415
+ // Pick the first --flag form for the display name (handles "positional|--path").
416
+ const flagName = a.from.split('|').map((s) => s.trim()).find((s) => s.startsWith('--')) ?? `--${a.name}`;
417
+ lines.push(` ${flagName}${ty}${req}${def}`);
418
+ }
419
+ lines.push('');
420
+ }
421
+ if (spec.uses && spec.uses.length > 0) {
422
+ lines.push(`Capabilities: ${spec.uses.join(', ')}`);
423
+ }
424
+ return lines.join('\n');
425
+ }
426
+ function listDocs(skill) {
427
+ const docDir = join(skill.installedAt, 'docs');
428
+ if (!existsSync(docDir))
429
+ return [];
430
+ const entries = [];
431
+ let names = [];
432
+ try {
433
+ names = readdirSync(docDir);
434
+ }
435
+ catch {
436
+ return [];
437
+ }
438
+ for (const file of names) {
439
+ if (!file.toLowerCase().endsWith('.md'))
440
+ continue;
441
+ const full = join(docDir, file);
442
+ try {
443
+ const raw = readFileSync(full, 'utf-8');
444
+ const fm = parseFrontmatter(raw);
445
+ const fallbackName = file.replace(/\.md$/i, '');
446
+ entries.push({
447
+ name: fm?.name ?? fallbackName,
448
+ label: fm?.label ?? fm?.name ?? fallbackName,
449
+ summary: fm?.summary ?? '',
450
+ tags: fm?.tags ?? [],
451
+ path: full,
452
+ });
453
+ }
454
+ catch { /* skip unreadable files */ }
455
+ }
456
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
457
+ }
458
+ function renderDocList(skill) {
459
+ const docs = listDocs(skill);
460
+ if (docs.length === 0) {
461
+ return [
462
+ `${skill.domain} — no templates / docs available.`,
463
+ `(skill maintainer hasn't shipped a docs/ folder)`,
464
+ ].join('\n');
465
+ }
466
+ const lines = [];
467
+ lines.push(`${skill.domain} — ${docs.length} template(s) available. Read one with \`${skill.domain} --doc <name>\`.`);
468
+ lines.push('');
469
+ lines.push(['NAME', 'LABEL', 'SUMMARY'].join('\t'));
470
+ for (const d of docs) {
471
+ lines.push([d.name, d.label, d.summary].join('\t'));
472
+ }
473
+ return lines.join('\n');
474
+ }
475
+ function renderDocOne(skill, name) {
476
+ const docs = listDocs(skill);
477
+ const doc = docs.find((d) => d.name === name)
478
+ ?? docs.find((d) => d.name.toLowerCase() === name.toLowerCase())
479
+ ?? docs.find((d) => d.label === name);
480
+ if (!doc) {
481
+ const known = docs.map((d) => d.name).join(', ') || '(none)';
482
+ return `${skill.domain} --doc: unknown template '${name}'.\nAvailable: ${known}`;
483
+ }
484
+ // Strip the frontmatter on output — the body is what the LLM consumes.
485
+ const raw = readFileSync(doc.path, 'utf-8');
486
+ const body = stripFrontmatter(raw);
487
+ return `# ${doc.label}\n\n${body.trim()}\n\n— from ${skill.domain} --doc ${doc.name}`;
488
+ }
489
+ /**
490
+ * Parse a `--- ... ---` YAML-ish frontmatter block. We only support a
491
+ * tiny subset (key: value, key: [v1, v2]) — enough for skill docs and
492
+ * trivially diffable. Returns null when the file has no frontmatter.
493
+ */
494
+ function parseFrontmatter(raw) {
495
+ const m = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
496
+ if (!m)
497
+ return null;
498
+ const obj = {};
499
+ for (const line of m[1].split(/\r?\n/)) {
500
+ const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
501
+ if (!kv)
502
+ continue;
503
+ const key = kv[1].trim();
504
+ let val = kv[2].trim();
505
+ // tiny array literal: [a, b, "c d"]
506
+ if (typeof val === 'string' && val.startsWith('[') && val.endsWith(']')) {
507
+ val = val
508
+ .slice(1, -1)
509
+ .split(',')
510
+ .map((s) => s.trim().replace(/^["']|["']$/g, ''))
511
+ .filter(Boolean);
512
+ }
513
+ else if (typeof val === 'string') {
514
+ val = val.replace(/^["']|["']$/g, '');
515
+ }
516
+ obj[key] = val;
517
+ }
518
+ if (typeof obj.name !== 'string')
519
+ return null;
520
+ return {
521
+ name: obj.name,
522
+ label: typeof obj.label === 'string' ? obj.label : undefined,
523
+ summary: typeof obj.summary === 'string' ? obj.summary : undefined,
524
+ tags: Array.isArray(obj.tags) ? obj.tags : undefined,
525
+ };
526
+ }
527
+ function stripFrontmatter(raw) {
528
+ return raw.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, '');
529
+ }
530
+ function renderBatchHelp(skill) {
531
+ return [
532
+ `${skill.domain} batch — run several actions in a single Bash call`,
533
+ ``,
534
+ `USAGE`,
535
+ ` ${skill.domain} batch [flags] <<TAG`,
536
+ ` <cmd1>`,
537
+ ` <cmd2>`,
538
+ ` ...`,
539
+ ` TAG`,
540
+ ``,
541
+ `or simple newline form:`,
542
+ ` ${skill.domain} batch [flags]`,
543
+ ` <cmd1>`,
544
+ ` <cmd2>`,
545
+ ``,
546
+ `FLAGS`,
547
+ ` --keep-going don't stop on first failure (default: fail-fast)`,
548
+ ` --merge render all sub-commands as one osascript invocation`,
549
+ ` (5-10x faster; skips Launch Services actions like 'open')`,
550
+ ``,
551
+ `BODY RULES`,
552
+ ` - One sub-command per line, NO domain prefix`,
553
+ ` - '# inline comments' and blank lines are skipped`,
554
+ ` - Quotes (\"...\" or '...') work the same as single-line commands`,
555
+ ` - Heredoc tag can be any word: <<EOF / <<BATCH / <<DONE`,
556
+ ``,
557
+ `EXAMPLE`,
558
+ ` ${skill.domain} batch <<EOF`,
559
+ ` new ~/Desktop/x.pptx`,
560
+ ` title 1 --text=\"Hello\"`,
561
+ ` slide.add`,
562
+ ` save`,
563
+ ` EOF`,
564
+ ].join('\n');
565
+ }
566
+ /**
567
+ * Detects + parses the two batch forms. Returns null when the line is
568
+ * NOT a batch invocation; returns a populated object otherwise (even on
569
+ * malformed heredoc — the runner will turn that into a useful error so
570
+ * we don't silently fall through to /bin/sh).
571
+ */
572
+ function parseBatchCommand(commandLine) {
573
+ const normalised = commandLine.replace(/\r\n/g, '\n').trim();
574
+ // Heredoc form. First line: `<domain> batch [flags] <<TAG`
575
+ // Body until matching TAG on its own line.
576
+ const heredoc = normalised.match(/^(\w+)\s+batch\b([^\n]*?)<<(\w+)[ \t]*\n([\s\S]*?)\n[ \t]*\3[ \t]*$/);
577
+ if (heredoc) {
578
+ const [, domain, headFlags, , body] = heredoc;
579
+ return { domain, body, flags: parseBatchFlags(headFlags) };
580
+ }
581
+ // Simple newline form. First line: `<domain> batch [flags]`. Body = rest.
582
+ const simple = normalised.match(/^(\w+)\s+batch\b([^\n]*)\n([\s\S]+)$/);
583
+ if (simple) {
584
+ const [, domain, headFlags, body] = simple;
585
+ return { domain, body, flags: parseBatchFlags(headFlags) };
586
+ }
587
+ // Bare `<domain> batch` with no body — likely a typo. Claim it so we
588
+ // can surface a help message rather than running it as a real shell
589
+ // command (which would just fail with `batch: command not found`).
590
+ const bare = normalised.match(/^(\w+)\s+batch\b([^\n]*)$/);
591
+ if (bare) {
592
+ const [, domain, headFlags] = bare;
593
+ return { domain, body: '', flags: parseBatchFlags(headFlags) };
594
+ }
595
+ return null;
596
+ }
597
+ function parseBatchFlags(headRest) {
598
+ const tokens = tokenize(headRest.trim());
599
+ return {
600
+ keepGoing: tokens.includes('--keep-going') || tokens.includes('-k'),
601
+ merge: tokens.includes('--merge'),
602
+ };
603
+ }
604
+ /**
605
+ * Strip a trailing inline `# comment` while respecting quotes.
606
+ * `#` is only a comment marker at line start or after whitespace, so
607
+ * `--color=#FF0000` doesn't get truncated.
608
+ */
609
+ function stripInlineComment(line) {
610
+ let inQuote = null;
611
+ for (let i = 0; i < line.length; i++) {
612
+ const c = line[i];
613
+ if (inQuote) {
614
+ if (c === inQuote)
615
+ inQuote = null;
616
+ continue;
617
+ }
618
+ if (c === '"' || c === "'") {
619
+ inQuote = c;
620
+ continue;
621
+ }
622
+ if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) {
623
+ return line.slice(0, i).trimEnd();
624
+ }
625
+ }
626
+ return line;
627
+ }
628
+ /**
629
+ * Tokenise + group each non-empty body line into a sub-command. We don't
630
+ * validate against the manifest here — the dispatcher does that, and we
631
+ * want the same error reporting whether the call came from batch or a
632
+ * single line.
633
+ */
634
+ function splitBatchBody(body) {
635
+ const out = [];
636
+ for (const rawLine of body.split('\n')) {
637
+ const stripped = stripInlineComment(rawLine).trim();
638
+ if (!stripped)
639
+ continue;
640
+ if (stripped.startsWith('#'))
641
+ continue;
642
+ const tokens = tokenize(stripped);
643
+ if (tokens.length === 0)
644
+ continue;
645
+ const action = tokens[0];
646
+ // Reuse the same argv parser we use for the single-command path.
647
+ const { positionals, flags } = parseArgs(tokens.slice(1));
648
+ out.push({ action, positionals, flags, raw: stripped });
649
+ }
650
+ return out;
651
+ }
652
+ async function runBatch(b) {
653
+ const installed = getInstalledFast().filter((s) => s.status === 'enabled');
654
+ const skill = installed.find((s) => s.domain === b.domain);
655
+ if (!skill) {
656
+ return err(`batch: unknown domain '${b.domain}' (no installed skill owns it)`, 127);
657
+ }
658
+ if (!b.body.trim()) {
659
+ return err(`batch: empty body. Usage:\n` +
660
+ ` ${b.domain} batch <<EOF\n <cmd1>\n <cmd2>\n EOF\n` +
661
+ `or\n` +
662
+ ` ${b.domain} batch\n <cmd1>\n <cmd2>\n` +
663
+ `Flags: --keep-going (don't stop on first failure), --merge (single osascript run)`, 2);
664
+ }
665
+ const subs = splitBatchBody(b.body);
666
+ if (subs.length === 0) {
667
+ return err(`batch: no executable commands found (only blank lines / comments?)`, 2);
668
+ }
669
+ if (b.flags.merge) {
670
+ return runBatchMerged(skill, subs, b.flags.keepGoing);
671
+ }
672
+ // Default: simple loop, fail-fast unless --keep-going.
673
+ const lines = [];
674
+ let lastErr = '';
675
+ let failed = 0;
676
+ for (let i = 0; i < subs.length; i++) {
677
+ const s = subs[i];
678
+ const r = await invokeSkill({
679
+ slugOrDomain: skill.slug,
680
+ action: s.action,
681
+ positionals: s.positionals,
682
+ flags: s.flags,
683
+ });
684
+ if (r.exitCode !== 0) {
685
+ failed++;
686
+ lastErr = `[line ${i + 1}: ${s.raw}] ${r.stderr || r.stdout || `exit ${r.exitCode}`}`;
687
+ lines.push(`✗ ${s.action}: ${r.stderr.trim() || r.stdout.trim() || `exit ${r.exitCode}`}`);
688
+ if (!b.flags.keepGoing)
689
+ break;
690
+ }
691
+ else {
692
+ const txt = (r.stdout || '').trim();
693
+ lines.push(`✓ ${s.action}${txt ? ': ' + txt : ''}`);
694
+ }
695
+ }
696
+ const stdout = lines.join('\n');
697
+ const summary = failed === 0
698
+ ? `\n— batch ok (${subs.length} commands)`
699
+ : `\n— batch finished: ${subs.length - failed}/${subs.length} ok, ${failed} failed`;
700
+ return {
701
+ handled: true,
702
+ stdout: stdout + summary,
703
+ stderr: lastErr,
704
+ exitCode: failed === 0 ? 0 : 1,
705
+ };
706
+ }
707
+ /**
708
+ * --merge path: render every sub-command's AppleScript template, splice
709
+ * them into a single source, and call osascript once. We import lazily
710
+ * (and from the dispatcher module so the merged path stays in one file).
711
+ */
712
+ async function runBatchMerged(skill, subs, keepGoing) {
713
+ const { dispatchMacosBatchMerged } = await import('./dispatcher.js');
714
+ const r = await dispatchMacosBatchMerged(skill.installedAt, subs.map((s) => ({ action: s.action, call: { positionals: s.positionals, flags: s.flags } })), { keepGoing });
715
+ return {
716
+ handled: true,
717
+ stdout: r.stdout,
718
+ stderr: r.stderr,
719
+ exitCode: r.exitCode,
720
+ };
721
+ }
722
+ // ── small helpers ───────────────────────────────────────────────────
723
+ function ok(out) {
724
+ return { handled: true, stdout: out, stderr: '', exitCode: 0 };
725
+ }
726
+ function err(msg, code = 1) {
727
+ return { handled: true, stdout: '', stderr: msg, exitCode: code };
728
+ }
729
+ function notHandled() {
730
+ return { handled: false, stdout: '', stderr: '', exitCode: 0 };
731
+ }
732
+ //# sourceMappingURL=kernel.js.map