@headways/cli 0.4.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -32
- package/dist/{api-5EKGGFQ6.js → api-H34ZX4FL.js} +2 -2
- package/dist/{chunk-UUFIIGTZ.js → chunk-COGZMSYS.js} +2 -0
- package/dist/{chunk-2INXZHRG.js → chunk-GN2N6M4B.js} +1 -1
- package/dist/{config-XQHAXREA.js → config-APIR4RCR.js} +3 -1
- package/dist/index.js +400 -143
- package/package.json +1 -1
- package/dist/chunk-XTEQBKIN.js +0 -409
- package/dist/sync-Q3OQUWOD.js +0 -16
package/dist/index.js
CHANGED
|
@@ -2,27 +2,68 @@
|
|
|
2
2
|
import {
|
|
3
3
|
apiRequest,
|
|
4
4
|
rawRequest
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import {
|
|
7
|
-
registerSetupCommand,
|
|
8
|
-
registerSyncCommands,
|
|
9
|
-
registerUninstallCommand
|
|
10
|
-
} from "./chunk-XTEQBKIN.js";
|
|
5
|
+
} from "./chunk-GN2N6M4B.js";
|
|
11
6
|
import {
|
|
12
7
|
CLAUDE_SKILLS_DIR,
|
|
13
8
|
INSTALLED_DIR,
|
|
9
|
+
RUNS_DIR,
|
|
14
10
|
getApiUrl,
|
|
15
11
|
getAppUrl,
|
|
16
12
|
readConfig,
|
|
17
13
|
requireAuth,
|
|
18
14
|
writeConfig
|
|
19
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-COGZMSYS.js";
|
|
20
16
|
|
|
21
17
|
// src/index.ts
|
|
22
18
|
import "dotenv/config";
|
|
23
|
-
import { createRequire } from "module";
|
|
24
19
|
import { program } from "commander";
|
|
25
20
|
|
|
21
|
+
// package.json
|
|
22
|
+
var package_default = {
|
|
23
|
+
name: "@headways/cli",
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
type: "module",
|
|
26
|
+
description: "Headways CLI \u2014 authoring, sync, and runtime SDK",
|
|
27
|
+
license: "MIT",
|
|
28
|
+
files: [
|
|
29
|
+
"dist",
|
|
30
|
+
"LICENSE",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
bin: {
|
|
34
|
+
headways: "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
publishConfig: {
|
|
37
|
+
access: "public"
|
|
38
|
+
},
|
|
39
|
+
scripts: {
|
|
40
|
+
build: "tsup",
|
|
41
|
+
dev: "tsx src/index.ts",
|
|
42
|
+
test: "vitest run",
|
|
43
|
+
"test:unit": "vitest run",
|
|
44
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
45
|
+
prepublishOnly: "pnpm build"
|
|
46
|
+
},
|
|
47
|
+
dependencies: {
|
|
48
|
+
"@headways/db": "workspace:*",
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
50
|
+
chalk: "^5.3.0",
|
|
51
|
+
commander: "^12.1.0",
|
|
52
|
+
dotenv: "^16.4.7",
|
|
53
|
+
"node-fetch": "^3.3.2",
|
|
54
|
+
yaml: "^2.5.1",
|
|
55
|
+
zod: "^3.25.28"
|
|
56
|
+
},
|
|
57
|
+
devDependencies: {
|
|
58
|
+
"@headways/config": "workspace:*",
|
|
59
|
+
"@types/node": "^22.16.5",
|
|
60
|
+
tsup: "^8.5.1",
|
|
61
|
+
tsx: "^4.21.0",
|
|
62
|
+
typescript: "^5.8.3",
|
|
63
|
+
vitest: "^3.2.4"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
26
67
|
// src/commands/auth.ts
|
|
27
68
|
import "commander";
|
|
28
69
|
import * as http from "http";
|
|
@@ -240,6 +281,12 @@ function registerNewCommand(program2) {
|
|
|
240
281
|
console.error("slug and headline are required.");
|
|
241
282
|
process.exit(1);
|
|
242
283
|
}
|
|
284
|
+
if (headline.length > 90) {
|
|
285
|
+
console.error(
|
|
286
|
+
`Headline is ${headline.length} chars \u2014 must be \u2264 90. Shorten to one sentence.`
|
|
287
|
+
);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
243
290
|
const dir = path.join(process.cwd(), slug);
|
|
244
291
|
await fs.mkdir(dir, { recursive: true });
|
|
245
292
|
await fs.writeFile(
|
|
@@ -311,96 +358,63 @@ runtimes: [claude-code]
|
|
|
311
358
|
|
|
312
359
|
// src/commands/skills/import.ts
|
|
313
360
|
import "commander";
|
|
361
|
+
import * as fs3 from "fs/promises";
|
|
362
|
+
import * as path3 from "path";
|
|
363
|
+
|
|
364
|
+
// src/commands/skills/push.ts
|
|
365
|
+
import "commander";
|
|
314
366
|
import * as fs2 from "fs/promises";
|
|
315
367
|
import * as path2 from "path";
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
program2.command("import <path>").description("Import a skill from a file, directory, or URL").option("--slug <slug>", "Override derived slug").option(
|
|
330
|
-
"--org <orgProfile>",
|
|
331
|
-
"Apply org-specific connector hints + fixture templates (hippocratic, revive)"
|
|
332
|
-
).action(async (inputPath, opts) => {
|
|
333
|
-
requireAuth();
|
|
334
|
-
let source;
|
|
335
|
-
let format = "auto";
|
|
368
|
+
import { watch, existsSync } from "fs";
|
|
369
|
+
var RESERVED_TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
370
|
+
"SKILL.md",
|
|
371
|
+
"headways.yaml",
|
|
372
|
+
"capabilities.yaml",
|
|
373
|
+
"connections.yaml",
|
|
374
|
+
"hooks.yaml"
|
|
375
|
+
]);
|
|
376
|
+
var IGNORE_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".gitkeep"]);
|
|
377
|
+
async function collectExtraFiles(dir) {
|
|
378
|
+
const result = {};
|
|
379
|
+
async function walk(current, prefix) {
|
|
380
|
+
let entries;
|
|
336
381
|
try {
|
|
337
|
-
|
|
338
|
-
if (stat2.isDirectory()) {
|
|
339
|
-
const skillMdPath = path2.join(inputPath, "SKILL.md");
|
|
340
|
-
source = await fs2.readFile(skillMdPath, "utf-8");
|
|
341
|
-
format = "skill-md";
|
|
342
|
-
} else {
|
|
343
|
-
source = await fs2.readFile(inputPath, "utf-8");
|
|
344
|
-
if (inputPath.endsWith(".yaml") || inputPath.endsWith(".yml")) {
|
|
345
|
-
format = "headways-yaml";
|
|
346
|
-
} else {
|
|
347
|
-
format = "markdown";
|
|
348
|
-
}
|
|
349
|
-
}
|
|
382
|
+
entries = await fs2.readdir(current, { withFileTypes: true });
|
|
350
383
|
} catch {
|
|
351
|
-
|
|
352
|
-
process.exit(1);
|
|
353
|
-
}
|
|
354
|
-
const profile = opts.org ? ORG_PROFILES[opts.org] : void 0;
|
|
355
|
-
if (opts.org && !profile) {
|
|
356
|
-
console.warn(
|
|
357
|
-
`Unknown org profile '${opts.org}'. Known profiles: ${Object.keys(ORG_PROFILES).join(", ")}`
|
|
358
|
-
);
|
|
384
|
+
return;
|
|
359
385
|
}
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
});
|
|
371
|
-
console.log(`Imported as '${result.slug}' (format: ${result.detectedFormat})`);
|
|
372
|
-
console.log(` Headline: ${result.headline}`);
|
|
373
|
-
console.log(` Skill ID: ${result.skillId}`);
|
|
374
|
-
if (profile) {
|
|
375
|
-
console.log(` Org profile: ${opts.org}`);
|
|
376
|
-
console.log(` Connector hints: ${profile.connectorHints.join(", ")}`);
|
|
377
|
-
console.log(` Fixture template: ${profile.fixtureTemplate}`);
|
|
386
|
+
for (const entry of entries) {
|
|
387
|
+
if (IGNORE_NAMES.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
388
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
389
|
+
const abs = path2.join(current, entry.name);
|
|
390
|
+
if (entry.isDirectory()) {
|
|
391
|
+
await walk(abs, rel);
|
|
392
|
+
} else if (!prefix && RESERVED_TOP_LEVEL.has(entry.name)) {
|
|
393
|
+
} else {
|
|
394
|
+
result[rel] = await fs2.readFile(abs, "utf-8");
|
|
395
|
+
}
|
|
378
396
|
}
|
|
379
|
-
|
|
380
|
-
|
|
397
|
+
}
|
|
398
|
+
await walk(dir, "");
|
|
399
|
+
return result;
|
|
381
400
|
}
|
|
382
|
-
|
|
383
|
-
// src/commands/skills/push.ts
|
|
384
|
-
import "commander";
|
|
385
|
-
import * as fs3 from "fs/promises";
|
|
386
|
-
import * as path3 from "path";
|
|
387
|
-
import { watch, existsSync } from "fs";
|
|
388
401
|
var catchMissing = (e) => {
|
|
389
402
|
if (e.code === "ENOENT") return null;
|
|
390
403
|
throw e;
|
|
391
404
|
};
|
|
392
405
|
async function readSkillDir(dir) {
|
|
393
|
-
const skillMdPath =
|
|
406
|
+
const skillMdPath = path2.join(dir, "SKILL.md");
|
|
394
407
|
let body;
|
|
395
408
|
try {
|
|
396
|
-
body = await
|
|
409
|
+
body = await fs2.readFile(skillMdPath, "utf-8");
|
|
397
410
|
} catch {
|
|
398
411
|
throw new Error(`SKILL.md not found in ${dir}`);
|
|
399
412
|
}
|
|
400
|
-
const [headwaysYaml, capabilitiesYaml, connectionsYaml] = await Promise.all([
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
413
|
+
const [headwaysYaml, capabilitiesYaml, connectionsYaml, hooksYaml] = await Promise.all([
|
|
414
|
+
fs2.readFile(path2.join(dir, "headways.yaml"), "utf-8").catch(catchMissing),
|
|
415
|
+
fs2.readFile(path2.join(dir, "capabilities.yaml"), "utf-8").catch(catchMissing),
|
|
416
|
+
fs2.readFile(path2.join(dir, "connections.yaml"), "utf-8").catch(catchMissing),
|
|
417
|
+
fs2.readFile(path2.join(dir, "hooks.yaml"), "utf-8").catch(catchMissing)
|
|
404
418
|
]);
|
|
405
419
|
let headline;
|
|
406
420
|
if (headwaysYaml) {
|
|
@@ -412,7 +426,15 @@ async function readSkillDir(dir) {
|
|
|
412
426
|
const items = parseConnectionsYaml(connectionsYaml);
|
|
413
427
|
if (items.length > 0) connections = items;
|
|
414
428
|
}
|
|
415
|
-
|
|
429
|
+
const extraFiles = await collectExtraFiles(dir);
|
|
430
|
+
return {
|
|
431
|
+
body,
|
|
432
|
+
headline,
|
|
433
|
+
capabilities: capabilitiesYaml ?? void 0,
|
|
434
|
+
hooks: hooksYaml ?? void 0,
|
|
435
|
+
connections,
|
|
436
|
+
files: Object.keys(extraFiles).length > 0 ? extraFiles : void 0
|
|
437
|
+
};
|
|
416
438
|
}
|
|
417
439
|
function parseConnectionsYaml(yaml) {
|
|
418
440
|
const items = [];
|
|
@@ -437,28 +459,36 @@ function parseConnectionsYaml(yaml) {
|
|
|
437
459
|
return items;
|
|
438
460
|
}
|
|
439
461
|
async function pushSkill(slug, dir) {
|
|
440
|
-
const { body, headline, capabilities, connections } = await readSkillDir(dir);
|
|
462
|
+
const { body, headline, capabilities, hooks, connections, files } = await readSkillDir(dir);
|
|
463
|
+
if (headline && headline.length > 90) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
`Headline is ${headline.length} chars \u2014 must be \u2264 90. Shorten to one sentence in headways.yaml.`
|
|
466
|
+
);
|
|
467
|
+
}
|
|
441
468
|
await apiRequest(`/v1/skills/${slug}/draft`, {
|
|
442
469
|
method: "PUT",
|
|
443
470
|
body: JSON.stringify({
|
|
444
471
|
body,
|
|
445
472
|
...headline ? { headline } : {},
|
|
446
473
|
...capabilities ? { capabilities } : {},
|
|
447
|
-
...
|
|
474
|
+
...hooks ? { hooks } : {},
|
|
475
|
+
...connections ? { connections } : {},
|
|
476
|
+
...files ? { files } : {}
|
|
448
477
|
})
|
|
449
478
|
});
|
|
450
|
-
|
|
479
|
+
const fileCount = files ? Object.keys(files).length : 0;
|
|
480
|
+
console.log(`Pushed '${slug}' draft${fileCount > 0 ? ` (${fileCount} extra file${fileCount === 1 ? "" : "s"})` : ""}`);
|
|
451
481
|
}
|
|
452
482
|
function resolveSkillDir(slug) {
|
|
453
483
|
if (!slug) return process.cwd();
|
|
454
|
-
const installedPath =
|
|
484
|
+
const installedPath = path2.join(CLAUDE_SKILLS_DIR, slug);
|
|
455
485
|
if (existsSync(installedPath)) return installedPath;
|
|
456
|
-
return
|
|
486
|
+
return path2.join(process.cwd(), slug);
|
|
457
487
|
}
|
|
458
488
|
function registerPushCommand(program2) {
|
|
459
489
|
program2.command("push [slug]").description("Push local skill files as a draft to Headways").option("--watch", "Watch for file changes and auto-push").option("--dir <dir>", "Skill directory (default: installed location, then ./<slug>)").action(async (slug, opts) => {
|
|
460
490
|
requireAuth();
|
|
461
|
-
const resolvedSlug = slug ??
|
|
491
|
+
const resolvedSlug = slug ?? path2.basename(process.cwd());
|
|
462
492
|
const dir = opts.dir ?? resolveSkillDir(slug);
|
|
463
493
|
await pushSkill(resolvedSlug, dir);
|
|
464
494
|
if (opts.watch) {
|
|
@@ -480,6 +510,86 @@ function registerPushCommand(program2) {
|
|
|
480
510
|
});
|
|
481
511
|
}
|
|
482
512
|
|
|
513
|
+
// src/commands/skills/import.ts
|
|
514
|
+
var ORG_PROFILES = {
|
|
515
|
+
hippocratic: {
|
|
516
|
+
connectorHints: ["ehr.read", "ehr.write", "phi.access"],
|
|
517
|
+
fixtureTemplate: "patient-encounter",
|
|
518
|
+
channelPolicy: "stable"
|
|
519
|
+
},
|
|
520
|
+
revive: {
|
|
521
|
+
connectorHints: ["email.read", "calendar.read", "ads.read"],
|
|
522
|
+
fixtureTemplate: "campaign-brief",
|
|
523
|
+
channelPolicy: "beta"
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
function registerImportCommand(program2) {
|
|
527
|
+
program2.command("import <path>").description("Import a skill from a file, directory, or URL").option("--slug <slug>", "Override derived slug").option(
|
|
528
|
+
"--org <orgProfile>",
|
|
529
|
+
"Apply org-specific connector hints + fixture templates (hippocratic, revive)"
|
|
530
|
+
).action(async (inputPath, opts) => {
|
|
531
|
+
requireAuth();
|
|
532
|
+
let source;
|
|
533
|
+
let format = "auto";
|
|
534
|
+
let extraFiles = {};
|
|
535
|
+
try {
|
|
536
|
+
const stat2 = await fs3.stat(inputPath);
|
|
537
|
+
if (stat2.isDirectory()) {
|
|
538
|
+
const skillMdPath = path3.join(inputPath, "SKILL.md");
|
|
539
|
+
source = await fs3.readFile(skillMdPath, "utf-8");
|
|
540
|
+
format = "skill-md";
|
|
541
|
+
extraFiles = await collectExtraFiles(inputPath);
|
|
542
|
+
} else {
|
|
543
|
+
source = await fs3.readFile(inputPath, "utf-8");
|
|
544
|
+
if (inputPath.endsWith(".yaml") || inputPath.endsWith(".yml")) {
|
|
545
|
+
format = "headways-yaml";
|
|
546
|
+
} else {
|
|
547
|
+
format = "markdown";
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch {
|
|
551
|
+
console.error(`Cannot read '${inputPath}': file or directory not found.`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
const profile = opts.org ? ORG_PROFILES[opts.org] : void 0;
|
|
555
|
+
if (opts.org && !profile) {
|
|
556
|
+
console.warn(
|
|
557
|
+
`Unknown org profile '${opts.org}'. Known profiles: ${Object.keys(ORG_PROFILES).join(", ")}`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const derivedSlug = opts.slug ?? path3.basename(inputPath, path3.extname(inputPath)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
561
|
+
const result = await apiRequest("/v1/skills/import", {
|
|
562
|
+
method: "POST",
|
|
563
|
+
body: JSON.stringify({
|
|
564
|
+
source,
|
|
565
|
+
format,
|
|
566
|
+
suggestedSlug: derivedSlug,
|
|
567
|
+
connectorHints: profile?.connectorHints,
|
|
568
|
+
channelPolicy: profile?.channelPolicy,
|
|
569
|
+
...Object.keys(extraFiles).length > 0 ? { files: extraFiles } : {}
|
|
570
|
+
})
|
|
571
|
+
});
|
|
572
|
+
if (result.headline.length > 90) {
|
|
573
|
+
console.warn(
|
|
574
|
+
` Warning: headline is ${result.headline.length} chars (\u2264 90 required to publish). Edit headways.yaml and run 'headways skills push ${result.slug}'.`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const fileCount = Object.keys(extraFiles).length;
|
|
578
|
+
console.log(`Imported as '${result.slug}' (format: ${result.detectedFormat})`);
|
|
579
|
+
console.log(` Headline: ${result.headline}`);
|
|
580
|
+
console.log(` Skill ID: ${result.skillId}`);
|
|
581
|
+
if (fileCount > 0) {
|
|
582
|
+
console.log(` Files: ${fileCount} extra file${fileCount === 1 ? "" : "s"} uploaded`);
|
|
583
|
+
}
|
|
584
|
+
if (profile) {
|
|
585
|
+
console.log(` Org profile: ${opts.org}`);
|
|
586
|
+
console.log(` Connector hints: ${profile.connectorHints.join(", ")}`);
|
|
587
|
+
console.log(` Fixture template: ${profile.fixtureTemplate}`);
|
|
588
|
+
}
|
|
589
|
+
console.log(` Run 'headways skills push ${result.slug}' to sync edits.`);
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
483
593
|
// src/commands/skills/index.ts
|
|
484
594
|
var SKILLS_GUIDE = `
|
|
485
595
|
# Headways Skill Authoring Guide
|
|
@@ -499,12 +609,12 @@ headways skills push <slug> # push local edits as a draft
|
|
|
499
609
|
| Field | Rule |
|
|
500
610
|
|----------------|----------------------------------------------------------------|
|
|
501
611
|
| \`slug\` | \`^[a-z0-9-]+$\`, 1\u201364 chars, immutable after creation |
|
|
502
|
-
| \`headline\` |
|
|
612
|
+
| \`headline\` | **One short sentence, \u2264 90 chars** \u2014 hard gate at submit |
|
|
503
613
|
| \`name\` | 1\u2013120 chars (display name, defaults to headline) |
|
|
504
614
|
| \`channel\` | \`prompt\` (default) | \`auto\` | \`manual\` |
|
|
505
615
|
| \`data_classes\` | \`none\` (default) | \`pii\` | \`phi\` | \`pci\` |
|
|
506
616
|
|
|
507
|
-
> Critical: headline must be \u2264 90 characters
|
|
617
|
+
> Critical: headline must be one short sentence, \u2264 90 characters. The web UI blocks submission if exceeded.
|
|
508
618
|
|
|
509
619
|
## File Bundle (\`<slug>/\`)
|
|
510
620
|
|
|
@@ -539,7 +649,7 @@ edge cases, and concrete examples. Vague goals produce poor results.
|
|
|
539
649
|
\`\`\`yaml
|
|
540
650
|
slug: my-skill
|
|
541
651
|
name: My Skill
|
|
542
|
-
headline:
|
|
652
|
+
headline: One short sentence, verb-first (\u2264 90 chars)
|
|
543
653
|
channel: prompt # prompt | auto | manual
|
|
544
654
|
runtimes:
|
|
545
655
|
- claude-code
|
|
@@ -557,8 +667,8 @@ auto_send: false # true = skill may act without user confirmation
|
|
|
557
667
|
|
|
558
668
|
### connections.yaml (required for any skill that uses MCP connector tools)
|
|
559
669
|
|
|
560
|
-
Declare every MCP connector the skill depends on. Users see this list
|
|
561
|
-
|
|
670
|
+
Declare every MCP connector the skill depends on. Users see this list when they install the skill
|
|
671
|
+
via the Headways desktop app, which gates installation on the connectors being configured.
|
|
562
672
|
|
|
563
673
|
\`\`\`yaml
|
|
564
674
|
- connector: slack # connector identifier (e.g. slack, github, atlassian, linear, notion, google-drive, stripe, asana, hubspot, datadog)
|
|
@@ -569,6 +679,38 @@ and the Headways app gates installation on the connectors being configured.
|
|
|
569
679
|
|
|
570
680
|
Omit the file entirely if the skill has no connector dependencies.
|
|
571
681
|
|
|
682
|
+
## Runtime Events
|
|
683
|
+
|
|
684
|
+
Skills can emit events during execution that show up in the Headways dashboard.
|
|
685
|
+
Two layers fire events:
|
|
686
|
+
|
|
687
|
+
1. **Automatic** \u2014 Claude Code hooks emit \`tool.*\` (per tool call) and \`turn.end\`
|
|
688
|
+
(per response turn) for every skill invocation. A \`skill.start\`-style record is created
|
|
689
|
+
by the desktop app's \`UserPromptSubmit\` hook when the user invokes \`/<slug>\`. You don't write these.
|
|
690
|
+
|
|
691
|
+
2. **Manual** \u2014 Inside your skill body, call \`headways emit\` to record concrete
|
|
692
|
+
outcomes the agent produced.
|
|
693
|
+
|
|
694
|
+
\`\`\`bash
|
|
695
|
+
headways emit --hook outcome.pr_created -f url=https://github.com/...
|
|
696
|
+
headways emit --hook outcome.email_sent -f to=user@example.com
|
|
697
|
+
headways emit --hook outcome.doc_written -f path=wiki/pages/x.md
|
|
698
|
+
\`\`\`
|
|
699
|
+
|
|
700
|
+
### Hook name format
|
|
701
|
+
|
|
702
|
+
All hook names must match \`<namespace>.<name>\` where namespace is one of:
|
|
703
|
+
|
|
704
|
+
| Namespace | Emitted by | Example |
|
|
705
|
+
|-------------|---------------|--------------------------------------|
|
|
706
|
+
| \`skill.*\` | Hook (auto) | \`skill.start\` |
|
|
707
|
+
| \`tool.*\` | Hook (auto) | \`tool.Read\`, \`tool.Bash\` |
|
|
708
|
+
| \`turn.*\` | Hook (auto) | \`turn.end\` |
|
|
709
|
+
| \`outcome.*\` | Skill (manual)| \`outcome.pr_created\`, \`outcome.email_sent\` |
|
|
710
|
+
|
|
711
|
+
Names outside these four namespaces are rejected with HTTP 422. Pick a short,
|
|
712
|
+
verb-past-tense suffix for outcomes: \`pr_created\`, not \`creating_pr\`.
|
|
713
|
+
|
|
572
714
|
### hooks.yaml (omit if unused)
|
|
573
715
|
|
|
574
716
|
\`\`\`yaml
|
|
@@ -621,7 +763,6 @@ local edits with \`headways skills push <slug>\`.
|
|
|
621
763
|
|
|
622
764
|
## Common Failure Modes
|
|
623
765
|
|
|
624
|
-
- Headline > 90 chars \u2192 submit blocked with 422. Shorten before pushing.
|
|
625
766
|
- Uppercase or special chars in slug \u2192 rejected at creation. Use \`a-z\`, \`0-9\`, \`-\` only.
|
|
626
767
|
- Missing \`capabilities.yaml\` entries \u2192 skill silently blocked at runtime.
|
|
627
768
|
- Missing \`connections.yaml\` for MCP-dependent skills \u2192 users install the skill but hit tool-not-found errors at runtime with no explanation. Always create this file when the skill calls MCP tools.
|
|
@@ -636,8 +777,8 @@ function registerSkillsCommands(program2) {
|
|
|
636
777
|
console.log(SKILLS_GUIDE);
|
|
637
778
|
});
|
|
638
779
|
skills.command("list").description("List skills in the active org").action(async () => {
|
|
639
|
-
const { requireAuth: requireAuth2 } = await import("./config-
|
|
640
|
-
const { apiRequest: apiRequest2 } = await import("./api-
|
|
780
|
+
const { requireAuth: requireAuth2 } = await import("./config-APIR4RCR.js");
|
|
781
|
+
const { apiRequest: apiRequest2 } = await import("./api-H34ZX4FL.js");
|
|
641
782
|
requireAuth2();
|
|
642
783
|
const result = await apiRequest2("/v1/skills");
|
|
643
784
|
if (result.data.length === 0) {
|
|
@@ -648,27 +789,6 @@ function registerSkillsCommands(program2) {
|
|
|
648
789
|
}
|
|
649
790
|
}
|
|
650
791
|
});
|
|
651
|
-
skills.command("accept <slug>").description("Accept a pending skill update and install it locally").action(async (slug) => {
|
|
652
|
-
const { acceptSkill } = await import("./sync-Q3OQUWOD.js");
|
|
653
|
-
await acceptSkill(slug);
|
|
654
|
-
try {
|
|
655
|
-
const { apiRequest: apiRequest2 } = await import("./api-5EKGGFQ6.js");
|
|
656
|
-
const metadata = await apiRequest2(`/v1/skills/${slug}/bundle/metadata`);
|
|
657
|
-
const reqs = metadata.connectionRequirements ?? [];
|
|
658
|
-
if (reqs.length > 0) {
|
|
659
|
-
console.log("");
|
|
660
|
-
console.log("This skill requires the following connectors:");
|
|
661
|
-
console.log("");
|
|
662
|
-
for (const req of reqs) {
|
|
663
|
-
console.log(` - ${req.connector.padEnd(20)} ${req.purpose}`);
|
|
664
|
-
}
|
|
665
|
-
console.log("");
|
|
666
|
-
console.log("To authorize these connectors, use the Headways desktop app");
|
|
667
|
-
console.log("or run: headways connections add <provider>");
|
|
668
|
-
}
|
|
669
|
-
} catch {
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
792
|
skills.command("feedback <slug>").description("Submit feedback about a skill").option(
|
|
673
793
|
"--reaction <type>",
|
|
674
794
|
"thumbs_up, thumbs_down, wrong_output, missing_step",
|
|
@@ -717,14 +837,95 @@ function registerConnectionsCommands(program2) {
|
|
|
717
837
|
|
|
718
838
|
// src/sdk/emit.ts
|
|
719
839
|
import "commander";
|
|
840
|
+
|
|
841
|
+
// src/lib/run-session.ts
|
|
842
|
+
import {
|
|
843
|
+
existsSync as existsSync2,
|
|
844
|
+
mkdirSync,
|
|
845
|
+
readdirSync,
|
|
846
|
+
readFileSync,
|
|
847
|
+
statSync,
|
|
848
|
+
unlinkSync,
|
|
849
|
+
writeFileSync
|
|
850
|
+
} from "fs";
|
|
851
|
+
import { join as join4 } from "path";
|
|
852
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
853
|
+
async function readHookStdin() {
|
|
854
|
+
if (process.stdin.isTTY) return null;
|
|
855
|
+
const chunks = [];
|
|
856
|
+
for await (const chunk of process.stdin) {
|
|
857
|
+
chunks.push(chunk);
|
|
858
|
+
}
|
|
859
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
860
|
+
if (!raw) return null;
|
|
861
|
+
try {
|
|
862
|
+
return JSON.parse(raw);
|
|
863
|
+
} catch {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function sessionFilePath(sessionId) {
|
|
868
|
+
return join4(RUNS_DIR, sessionId);
|
|
869
|
+
}
|
|
870
|
+
function writeSessionRun(sessionId, run) {
|
|
871
|
+
if (!existsSync2(RUNS_DIR)) mkdirSync(RUNS_DIR, { recursive: true });
|
|
872
|
+
writeFileSync(sessionFilePath(sessionId), JSON.stringify(run));
|
|
873
|
+
}
|
|
874
|
+
function readSessionRun(sessionId) {
|
|
875
|
+
const path4 = sessionFilePath(sessionId);
|
|
876
|
+
if (!existsSync2(path4)) return null;
|
|
877
|
+
try {
|
|
878
|
+
return JSON.parse(readFileSync(path4, "utf8"));
|
|
879
|
+
} catch {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
function cleanupStaleRuns(now = Date.now()) {
|
|
884
|
+
if (!existsSync2(RUNS_DIR)) return;
|
|
885
|
+
let entries;
|
|
886
|
+
try {
|
|
887
|
+
entries = readdirSync(RUNS_DIR);
|
|
888
|
+
} catch {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
for (const name of entries) {
|
|
892
|
+
const path4 = join4(RUNS_DIR, name);
|
|
893
|
+
try {
|
|
894
|
+
const stat2 = statSync(path4);
|
|
895
|
+
if (now - stat2.mtimeMs > THIRTY_DAYS_MS) unlinkSync(path4);
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/sdk/emit.ts
|
|
902
|
+
var HOOK_NAME_RE = /^(skill|tool|turn|outcome)\.[A-Za-z0-9_]+$/;
|
|
903
|
+
var HOOK_NAME_HELP = "Hook name must be <namespace>.<name> where namespace is one of: skill, tool, turn, outcome.\n skill.* lifecycle (e.g. skill.start)\n tool.* tool call events (e.g. tool.Read, tool.Bash)\n turn.* conversation turns (e.g. turn.end)\n outcome.* concrete artifacts (e.g. outcome.pr_created, outcome.email_sent)";
|
|
720
904
|
function registerEmitCommand(program2) {
|
|
721
|
-
program2.command("emit").description(
|
|
905
|
+
program2.command("emit").description(
|
|
906
|
+
"Emit a skill run event (used by Claude Code hooks and from inside skills).\n" + HOOK_NAME_HELP
|
|
907
|
+
).option("--run-id <runId>", "Skill run ID (or set HEADWAYS_RUN_ID env var)").option("--hook <hookName>", "Hook name to emit, e.g. outcome.pr_created").option("--event <event>", "Legacy alias for --hook").option("--status <status>", "Tool result status (true=error, false=ok)").option("-f, --field <entries...>", "Field values in key=value format").allowUnknownOption(true).action(
|
|
722
908
|
async (opts) => {
|
|
909
|
+
const hookName = opts.hook ?? opts.event ?? "tool.unknown";
|
|
910
|
+
if (!HOOK_NAME_RE.test(hookName)) {
|
|
911
|
+
console.error(`headways emit: invalid --hook "${hookName}"
|
|
912
|
+
|
|
913
|
+
${HOOK_NAME_HELP}`);
|
|
914
|
+
process.exitCode = 1;
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
723
917
|
const cfg = readConfig();
|
|
724
918
|
if (!cfg.token) return;
|
|
725
|
-
|
|
919
|
+
let runId = opts.runId ?? process.env["HEADWAYS_RUN_ID"];
|
|
920
|
+
if (!runId) {
|
|
921
|
+
const input = await readHookStdin();
|
|
922
|
+
const sessionId = input?.session_id;
|
|
923
|
+
if (typeof sessionId === "string") {
|
|
924
|
+
const session = readSessionRun(sessionId);
|
|
925
|
+
if (session) runId = session.runId;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
726
928
|
if (!runId) return;
|
|
727
|
-
const hookName = opts.hook ?? opts.event ?? "tool.unknown";
|
|
728
929
|
const payload = {};
|
|
729
930
|
if (opts.status !== void 0) {
|
|
730
931
|
payload["tool_result_is_error"] = opts.status === "true" || opts.status === "1";
|
|
@@ -755,8 +956,67 @@ function registerEmitCommand(program2) {
|
|
|
755
956
|
);
|
|
756
957
|
}
|
|
757
958
|
|
|
959
|
+
// src/commands/skill-run.ts
|
|
960
|
+
import { existsSync as existsSync3 } from "fs";
|
|
961
|
+
import { join as join5 } from "path";
|
|
962
|
+
import "commander";
|
|
963
|
+
var SLASH_COMMAND_RE = /^\/([a-z0-9-]+(?::[a-z0-9-]+)?)\b/;
|
|
964
|
+
function resolveSkillSlug(input, isInstalled) {
|
|
965
|
+
const direct = input.tool_input?.skill;
|
|
966
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
967
|
+
if (typeof input.prompt === "string") {
|
|
968
|
+
const m = SLASH_COMMAND_RE.exec(input.prompt);
|
|
969
|
+
if (m && m[1]) {
|
|
970
|
+
const slug = m[1];
|
|
971
|
+
if (isInstalled(slug)) return slug;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
function defaultIsInstalled(slug) {
|
|
977
|
+
return existsSync3(join5(INSTALLED_DIR, `${slug}.json`));
|
|
978
|
+
}
|
|
979
|
+
function registerSkillRunCommands(program2) {
|
|
980
|
+
const skillRun = program2.command("skill-run").description("Skill-run lifecycle commands (invoked by Claude Code hooks).");
|
|
981
|
+
skillRun.command("start").description(
|
|
982
|
+
"Create a skill run from a Claude Code hook. Accepts UserPromptSubmit (parses /<slug> from prompt) or PreToolUse(Skill) (tool_input.skill). POSTs /v1/skill-runs and writes ~/.headways/runs/<session_id>."
|
|
983
|
+
).action(async () => {
|
|
984
|
+
cleanupStaleRuns();
|
|
985
|
+
const cfg = readConfig();
|
|
986
|
+
if (!cfg.token) return;
|
|
987
|
+
const input = await readHookStdin();
|
|
988
|
+
if (!input) return;
|
|
989
|
+
const sessionId = input.session_id;
|
|
990
|
+
if (typeof sessionId !== "string") return;
|
|
991
|
+
const skillSlug = resolveSkillSlug(input, defaultIsInstalled);
|
|
992
|
+
if (!skillSlug) return;
|
|
993
|
+
let runId;
|
|
994
|
+
try {
|
|
995
|
+
const res = await fetch(`${getApiUrl()}/v1/skill-runs`, {
|
|
996
|
+
method: "POST",
|
|
997
|
+
headers: {
|
|
998
|
+
"Content-Type": "application/json",
|
|
999
|
+
Authorization: `Bearer ${cfg.token}`
|
|
1000
|
+
},
|
|
1001
|
+
body: JSON.stringify({ skillSlug, runtime: "claude-code" })
|
|
1002
|
+
});
|
|
1003
|
+
if (!res.ok) return;
|
|
1004
|
+
const body = await res.json();
|
|
1005
|
+
if (!body.runId) return;
|
|
1006
|
+
runId = body.runId;
|
|
1007
|
+
} catch {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
writeSessionRun(sessionId, {
|
|
1011
|
+
runId,
|
|
1012
|
+
skill: skillSlug,
|
|
1013
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
758
1018
|
// src/commands/prime.ts
|
|
759
|
-
import { existsSync as
|
|
1019
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
760
1020
|
import "commander";
|
|
761
1021
|
function registerPrimeCommand(program2) {
|
|
762
1022
|
program2.command("prime").description("Output Headways workflow context for AI coding assistants").action(() => {
|
|
@@ -772,6 +1032,11 @@ function registerPrimeCommand(program2) {
|
|
|
772
1032
|
"> `--help` to discover full options, or run `headways skills guide` before authoring a skill.",
|
|
773
1033
|
"> Do not guess at flags or constraints \u2014 discover them at runtime.",
|
|
774
1034
|
"",
|
|
1035
|
+
"## Responsibility split",
|
|
1036
|
+
"",
|
|
1037
|
+
"- **Desktop app (macOS):** installs skills, syncs the catalog, manages Claude Code hooks. Open it from `/Applications/Headways.app`.",
|
|
1038
|
+
"- **CLI (this tool):** skill *authoring* (push, import, new, list) plus runtime helpers Claude Code shells out to (`prime`, `emit`, `skill-run start`).",
|
|
1039
|
+
"",
|
|
775
1040
|
"## Auth & Config",
|
|
776
1041
|
"",
|
|
777
1042
|
`Status: ${cfg.token ? `Signed in (org: ${cfg.orgSlug ?? cfg.orgId ?? "unknown"})` : "Not signed in \u2014 run `headways login`"}`,
|
|
@@ -783,37 +1048,33 @@ function registerPrimeCommand(program2) {
|
|
|
783
1048
|
"headways login # Browser SSO sign-in",
|
|
784
1049
|
"headways logout # Remove stored credentials",
|
|
785
1050
|
"headways config status # Show saved key, org, URLs",
|
|
786
|
-
"headways config clear # Clear credentials
|
|
787
|
-
"",
|
|
788
|
-
"headways sync start # Pull catalog updates once",
|
|
789
|
-
"headways sync start --daemon # Poll every 60s in background",
|
|
790
|
-
"headways sync status # Show pending skill updates",
|
|
1051
|
+
"headways config clear # Clear credentials",
|
|
791
1052
|
"",
|
|
792
1053
|
"headways skills list # List skills in your org",
|
|
793
1054
|
"headways skills new # Scaffold a new skill",
|
|
794
1055
|
"headways skills import <path> # Create a new skill from a local file or directory",
|
|
795
1056
|
"headways skills push <slug> # Push edits to an existing skill (import or new first)",
|
|
796
|
-
"headways skills accept <slug> # Install a pending skill update",
|
|
797
1057
|
"headways skills feedback <slug> # Submit feedback on a skill",
|
|
798
1058
|
"headways skills guide # Authoring reference (run before creating a skill)",
|
|
799
1059
|
"",
|
|
800
|
-
"
|
|
801
|
-
"headways prime # Print this context
|
|
1060
|
+
"# Runtime helpers (invoked automatically by Claude Code hooks; rarely run by hand):",
|
|
1061
|
+
"headways prime # Print this context",
|
|
1062
|
+
"headways emit # Emit a skill-run event",
|
|
1063
|
+
"headways skill-run start # Start a skill run from a UserPromptSubmit hook",
|
|
802
1064
|
"```",
|
|
803
1065
|
"",
|
|
804
1066
|
"## Workflow",
|
|
805
1067
|
"",
|
|
806
|
-
"1.
|
|
807
|
-
"2.
|
|
808
|
-
"3.
|
|
809
|
-
"4. Run `headways sync start --daemon` to keep skills up to date in the background",
|
|
1068
|
+
"1. Open the **Headways desktop app** \u2014 it syncs the catalog, shows pending updates, and clicks-to-install bundles.",
|
|
1069
|
+
"2. New skill versions appear in the Library view; click **Install** to bring them onto this machine.",
|
|
1070
|
+
"3. To author skills: `headways skills new` or `headways skills import <path>`, edit, then `headways skills push <slug>`. Publish via the web UI.",
|
|
810
1071
|
"",
|
|
811
1072
|
"## Installed Skills",
|
|
812
1073
|
""
|
|
813
1074
|
];
|
|
814
1075
|
if (skills.length === 0) {
|
|
815
1076
|
lines.push(
|
|
816
|
-
"No skills installed.
|
|
1077
|
+
"No skills installed. Open the Headways desktop app to sync your org's catalog."
|
|
817
1078
|
);
|
|
818
1079
|
} else {
|
|
819
1080
|
for (const skill of skills) {
|
|
@@ -832,12 +1093,12 @@ function registerPrimeCommand(program2) {
|
|
|
832
1093
|
});
|
|
833
1094
|
}
|
|
834
1095
|
function getInstalledSkills() {
|
|
835
|
-
if (!
|
|
1096
|
+
if (!existsSync4(INSTALLED_DIR)) return [];
|
|
836
1097
|
try {
|
|
837
|
-
return
|
|
1098
|
+
return readdirSync2(INSTALLED_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
838
1099
|
const slug = f.replace(/\.json$/, "");
|
|
839
1100
|
try {
|
|
840
|
-
const raw = JSON.parse(
|
|
1101
|
+
const raw = JSON.parse(readFileSync2(`${INSTALLED_DIR}/${f}`, "utf8"));
|
|
841
1102
|
return {
|
|
842
1103
|
slug,
|
|
843
1104
|
version: String(raw.version ?? ""),
|
|
@@ -855,15 +1116,11 @@ function getInstalledSkills() {
|
|
|
855
1116
|
}
|
|
856
1117
|
|
|
857
1118
|
// src/index.ts
|
|
858
|
-
|
|
859
|
-
var { version } = require2("../package.json");
|
|
860
|
-
program.name("headways").description("Headways CLI \u2014 skill authoring, sync, and runtime SDK").version(version);
|
|
1119
|
+
program.name("headways").description("Headways CLI \u2014 skill authoring + Claude Code runtime helpers").version(package_default.version);
|
|
861
1120
|
registerAuthCommands(program);
|
|
862
1121
|
registerSkillsCommands(program);
|
|
863
1122
|
registerConnectionsCommands(program);
|
|
864
|
-
registerSyncCommands(program);
|
|
865
1123
|
registerEmitCommand(program);
|
|
1124
|
+
registerSkillRunCommands(program);
|
|
866
1125
|
registerPrimeCommand(program);
|
|
867
|
-
registerSetupCommand(program);
|
|
868
|
-
registerUninstallCommand(program);
|
|
869
1126
|
program.parse();
|