@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/dist/index.js CHANGED
@@ -2,27 +2,68 @@
2
2
  import {
3
3
  apiRequest,
4
4
  rawRequest
5
- } from "./chunk-2INXZHRG.js";
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-UUFIIGTZ.js";
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
- var ORG_PROFILES = {
317
- hippocratic: {
318
- connectorHints: ["ehr.read", "ehr.write", "phi.access"],
319
- fixtureTemplate: "patient-encounter",
320
- channelPolicy: "stable"
321
- },
322
- revive: {
323
- connectorHints: ["email.read", "calendar.read", "ads.read"],
324
- fixtureTemplate: "campaign-brief",
325
- channelPolicy: "beta"
326
- }
327
- };
328
- function registerImportCommand(program2) {
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
- const stat2 = await fs2.stat(inputPath);
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
- console.error(`Cannot read '${inputPath}': file or directory not found.`);
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 derivedSlug = opts.slug ?? path2.basename(inputPath, path2.extname(inputPath)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
361
- const result = await apiRequest("/v1/skills/import", {
362
- method: "POST",
363
- body: JSON.stringify({
364
- source,
365
- format,
366
- suggestedSlug: derivedSlug,
367
- connectorHints: profile?.connectorHints,
368
- channelPolicy: profile?.channelPolicy
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
- console.log(` Run 'headways skills push ${result.slug}' to sync edits.`);
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 = path3.join(dir, "SKILL.md");
406
+ const skillMdPath = path2.join(dir, "SKILL.md");
394
407
  let body;
395
408
  try {
396
- body = await fs3.readFile(skillMdPath, "utf-8");
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
- fs3.readFile(path3.join(dir, "headways.yaml"), "utf-8").catch(catchMissing),
402
- fs3.readFile(path3.join(dir, "capabilities.yaml"), "utf-8").catch(catchMissing),
403
- fs3.readFile(path3.join(dir, "connections.yaml"), "utf-8").catch(catchMissing)
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
- return { body, headline, capabilities: capabilitiesYaml ?? void 0, connections };
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
- ...connections ? { connections } : {}
474
+ ...hooks ? { hooks } : {},
475
+ ...connections ? { connections } : {},
476
+ ...files ? { files } : {}
448
477
  })
449
478
  });
450
- console.log(`Pushed '${slug}' draft`);
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 = path3.join(CLAUDE_SKILLS_DIR, slug);
484
+ const installedPath = path2.join(CLAUDE_SKILLS_DIR, slug);
455
485
  if (existsSync(installedPath)) return installedPath;
456
- return path3.join(process.cwd(), slug);
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 ?? path3.basename(process.cwd());
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\` | 1\u2013200 chars at creation; **\u2264 90 chars to submit** (hard gate) |
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 or the web UI will block submission.
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: Verb-first summary of the outcome (\u226490 chars)
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 on \`headways skills accept\`
561
- and the Headways app gates installation on the connectors being configured.
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-XQHAXREA.js");
640
- const { apiRequest: apiRequest2 } = await import("./api-5EKGGFQ6.js");
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("Emit a skill run event (used by Claude Code hooks)").option("--run-id <runId>", "Skill run ID (or set HEADWAYS_RUN_ID env var)").option("--hook <hookName>", "Hook name to emit").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(
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
- const runId = opts.runId ?? process.env["HEADWAYS_RUN_ID"];
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 existsSync2, readdirSync, readFileSync } from "fs";
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 + reset setup state",
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
- "headways setup claude # Install Claude Code hooks (SessionStart + PreCompact)",
801
- "headways prime # Print this context (used by hooks)",
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. `headways sync start` \u2014 pull the latest catalog from your org",
807
- "2. `headways accept <skill>` \u2014 install a skill locally",
808
- "3. Skills are automatically available to Claude Code via `~/.claude/skills/<slug>/`",
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. Run `headways sync start` then `headways accept <skill>`."
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 (!existsSync2(INSTALLED_DIR)) return [];
1096
+ if (!existsSync4(INSTALLED_DIR)) return [];
836
1097
  try {
837
- return readdirSync(INSTALLED_DIR).filter((f) => f.endsWith(".json")).map((f) => {
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(readFileSync(`${INSTALLED_DIR}/${f}`, "utf8"));
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
- var require2 = createRequire(import.meta.url);
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();