@headways/cli 0.4.2 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +488 -186
- 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,72 +2,22 @@
|
|
|
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
19
|
import { program } from "commander";
|
|
24
20
|
|
|
25
|
-
// package.json
|
|
26
|
-
var package_default = {
|
|
27
|
-
name: "@headways/cli",
|
|
28
|
-
version: "0.4.2",
|
|
29
|
-
type: "module",
|
|
30
|
-
description: "Headways CLI \u2014 authoring, sync, and runtime SDK",
|
|
31
|
-
license: "MIT",
|
|
32
|
-
files: [
|
|
33
|
-
"dist",
|
|
34
|
-
"LICENSE",
|
|
35
|
-
"README.md"
|
|
36
|
-
],
|
|
37
|
-
bin: {
|
|
38
|
-
headways: "./dist/index.js"
|
|
39
|
-
},
|
|
40
|
-
publishConfig: {
|
|
41
|
-
access: "public"
|
|
42
|
-
},
|
|
43
|
-
scripts: {
|
|
44
|
-
build: "tsup",
|
|
45
|
-
dev: "tsx src/index.ts",
|
|
46
|
-
test: "vitest run",
|
|
47
|
-
"test:unit": "vitest run",
|
|
48
|
-
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
49
|
-
prepublishOnly: "pnpm build"
|
|
50
|
-
},
|
|
51
|
-
dependencies: {
|
|
52
|
-
"@headways/db": "workspace:*",
|
|
53
|
-
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
54
|
-
chalk: "^5.3.0",
|
|
55
|
-
commander: "^12.1.0",
|
|
56
|
-
dotenv: "^16.4.7",
|
|
57
|
-
"node-fetch": "^3.3.2",
|
|
58
|
-
yaml: "^2.5.1",
|
|
59
|
-
zod: "^3.25.28"
|
|
60
|
-
},
|
|
61
|
-
devDependencies: {
|
|
62
|
-
"@headways/config": "workspace:*",
|
|
63
|
-
"@types/node": "^22.16.5",
|
|
64
|
-
tsup: "^8.5.1",
|
|
65
|
-
tsx: "^4.21.0",
|
|
66
|
-
typescript: "^5.8.3",
|
|
67
|
-
vitest: "^3.2.4"
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
21
|
// src/commands/auth.ts
|
|
72
22
|
import "commander";
|
|
73
23
|
import * as http from "http";
|
|
@@ -285,6 +235,12 @@ function registerNewCommand(program2) {
|
|
|
285
235
|
console.error("slug and headline are required.");
|
|
286
236
|
process.exit(1);
|
|
287
237
|
}
|
|
238
|
+
if (headline.length > 90) {
|
|
239
|
+
console.error(
|
|
240
|
+
`Headline is ${headline.length} chars \u2014 must be \u2264 90. Shorten to one sentence.`
|
|
241
|
+
);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
288
244
|
const dir = path.join(process.cwd(), slug);
|
|
289
245
|
await fs.mkdir(dir, { recursive: true });
|
|
290
246
|
await fs.writeFile(
|
|
@@ -356,96 +312,64 @@ runtimes: [claude-code]
|
|
|
356
312
|
|
|
357
313
|
// src/commands/skills/import.ts
|
|
358
314
|
import "commander";
|
|
315
|
+
import * as fs3 from "fs/promises";
|
|
316
|
+
import * as path3 from "path";
|
|
317
|
+
|
|
318
|
+
// src/commands/skills/push.ts
|
|
319
|
+
import "commander";
|
|
359
320
|
import * as fs2 from "fs/promises";
|
|
360
321
|
import * as path2 from "path";
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
};
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
"--org <orgProfile>",
|
|
376
|
-
"Apply org-specific connector hints + fixture templates (hippocratic, revive)"
|
|
377
|
-
).action(async (inputPath, opts) => {
|
|
378
|
-
requireAuth();
|
|
379
|
-
let source;
|
|
380
|
-
let format = "auto";
|
|
322
|
+
import { watch, existsSync } from "fs";
|
|
323
|
+
import * as YAML from "yaml";
|
|
324
|
+
var RESERVED_TOP_LEVEL = /* @__PURE__ */ new Set([
|
|
325
|
+
"SKILL.md",
|
|
326
|
+
"headways.yaml",
|
|
327
|
+
"capabilities.yaml",
|
|
328
|
+
"connections.yaml",
|
|
329
|
+
"hooks.yaml"
|
|
330
|
+
]);
|
|
331
|
+
var IGNORE_NAMES = /* @__PURE__ */ new Set([".git", "node_modules", ".DS_Store", ".gitkeep"]);
|
|
332
|
+
async function collectExtraFiles(dir) {
|
|
333
|
+
const result = {};
|
|
334
|
+
async function walk(current, prefix) {
|
|
335
|
+
let entries;
|
|
381
336
|
try {
|
|
382
|
-
|
|
383
|
-
if (stat2.isDirectory()) {
|
|
384
|
-
const skillMdPath = path2.join(inputPath, "SKILL.md");
|
|
385
|
-
source = await fs2.readFile(skillMdPath, "utf-8");
|
|
386
|
-
format = "skill-md";
|
|
387
|
-
} else {
|
|
388
|
-
source = await fs2.readFile(inputPath, "utf-8");
|
|
389
|
-
if (inputPath.endsWith(".yaml") || inputPath.endsWith(".yml")) {
|
|
390
|
-
format = "headways-yaml";
|
|
391
|
-
} else {
|
|
392
|
-
format = "markdown";
|
|
393
|
-
}
|
|
394
|
-
}
|
|
337
|
+
entries = await fs2.readdir(current, { withFileTypes: true });
|
|
395
338
|
} catch {
|
|
396
|
-
|
|
397
|
-
process.exit(1);
|
|
398
|
-
}
|
|
399
|
-
const profile = opts.org ? ORG_PROFILES[opts.org] : void 0;
|
|
400
|
-
if (opts.org && !profile) {
|
|
401
|
-
console.warn(
|
|
402
|
-
`Unknown org profile '${opts.org}'. Known profiles: ${Object.keys(ORG_PROFILES).join(", ")}`
|
|
403
|
-
);
|
|
339
|
+
return;
|
|
404
340
|
}
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
});
|
|
416
|
-
console.log(`Imported as '${result.slug}' (format: ${result.detectedFormat})`);
|
|
417
|
-
console.log(` Headline: ${result.headline}`);
|
|
418
|
-
console.log(` Skill ID: ${result.skillId}`);
|
|
419
|
-
if (profile) {
|
|
420
|
-
console.log(` Org profile: ${opts.org}`);
|
|
421
|
-
console.log(` Connector hints: ${profile.connectorHints.join(", ")}`);
|
|
422
|
-
console.log(` Fixture template: ${profile.fixtureTemplate}`);
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
if (IGNORE_NAMES.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
343
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
344
|
+
const abs = path2.join(current, entry.name);
|
|
345
|
+
if (entry.isDirectory()) {
|
|
346
|
+
await walk(abs, rel);
|
|
347
|
+
} else if (!prefix && RESERVED_TOP_LEVEL.has(entry.name)) {
|
|
348
|
+
} else {
|
|
349
|
+
result[rel] = await fs2.readFile(abs, "utf-8");
|
|
350
|
+
}
|
|
423
351
|
}
|
|
424
|
-
|
|
425
|
-
|
|
352
|
+
}
|
|
353
|
+
await walk(dir, "");
|
|
354
|
+
return result;
|
|
426
355
|
}
|
|
427
|
-
|
|
428
|
-
// src/commands/skills/push.ts
|
|
429
|
-
import "commander";
|
|
430
|
-
import * as fs3 from "fs/promises";
|
|
431
|
-
import * as path3 from "path";
|
|
432
|
-
import { watch, existsSync } from "fs";
|
|
433
356
|
var catchMissing = (e) => {
|
|
434
357
|
if (e.code === "ENOENT") return null;
|
|
435
358
|
throw e;
|
|
436
359
|
};
|
|
437
360
|
async function readSkillDir(dir) {
|
|
438
|
-
const skillMdPath =
|
|
361
|
+
const skillMdPath = path2.join(dir, "SKILL.md");
|
|
439
362
|
let body;
|
|
440
363
|
try {
|
|
441
|
-
body = await
|
|
364
|
+
body = await fs2.readFile(skillMdPath, "utf-8");
|
|
442
365
|
} catch {
|
|
443
366
|
throw new Error(`SKILL.md not found in ${dir}`);
|
|
444
367
|
}
|
|
445
|
-
const [headwaysYaml, capabilitiesYaml, connectionsYaml] = await Promise.all([
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
368
|
+
const [headwaysYaml, capabilitiesYaml, connectionsYaml, hooksYaml] = await Promise.all([
|
|
369
|
+
fs2.readFile(path2.join(dir, "headways.yaml"), "utf-8").catch(catchMissing),
|
|
370
|
+
fs2.readFile(path2.join(dir, "capabilities.yaml"), "utf-8").catch(catchMissing),
|
|
371
|
+
fs2.readFile(path2.join(dir, "connections.yaml"), "utf-8").catch(catchMissing),
|
|
372
|
+
fs2.readFile(path2.join(dir, "hooks.yaml"), "utf-8").catch(catchMissing)
|
|
449
373
|
]);
|
|
450
374
|
let headline;
|
|
451
375
|
if (headwaysYaml) {
|
|
@@ -457,7 +381,35 @@ async function readSkillDir(dir) {
|
|
|
457
381
|
const items = parseConnectionsYaml(connectionsYaml);
|
|
458
382
|
if (items.length > 0) connections = items;
|
|
459
383
|
}
|
|
460
|
-
|
|
384
|
+
let capabilities;
|
|
385
|
+
if (capabilitiesYaml) {
|
|
386
|
+
try {
|
|
387
|
+
capabilities = YAML.parse(capabilitiesYaml);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`Failed to parse capabilities.yaml: ${err instanceof Error ? err.message : String(err)}`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
let hooks;
|
|
395
|
+
if (hooksYaml) {
|
|
396
|
+
try {
|
|
397
|
+
hooks = YAML.parse(hooksYaml);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Failed to parse hooks.yaml: ${err instanceof Error ? err.message : String(err)}`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const extraFiles = await collectExtraFiles(dir);
|
|
405
|
+
return {
|
|
406
|
+
body,
|
|
407
|
+
headline,
|
|
408
|
+
capabilities,
|
|
409
|
+
hooks,
|
|
410
|
+
connections,
|
|
411
|
+
files: Object.keys(extraFiles).length > 0 ? extraFiles : void 0
|
|
412
|
+
};
|
|
461
413
|
}
|
|
462
414
|
function parseConnectionsYaml(yaml) {
|
|
463
415
|
const items = [];
|
|
@@ -482,28 +434,36 @@ function parseConnectionsYaml(yaml) {
|
|
|
482
434
|
return items;
|
|
483
435
|
}
|
|
484
436
|
async function pushSkill(slug, dir) {
|
|
485
|
-
const { body, headline, capabilities, connections } = await readSkillDir(dir);
|
|
437
|
+
const { body, headline, capabilities, hooks, connections, files } = await readSkillDir(dir);
|
|
438
|
+
if (headline && headline.length > 90) {
|
|
439
|
+
throw new Error(
|
|
440
|
+
`Headline is ${headline.length} chars \u2014 must be \u2264 90. Shorten to one sentence in headways.yaml.`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
486
443
|
await apiRequest(`/v1/skills/${slug}/draft`, {
|
|
487
444
|
method: "PUT",
|
|
488
445
|
body: JSON.stringify({
|
|
489
446
|
body,
|
|
490
447
|
...headline ? { headline } : {},
|
|
491
448
|
...capabilities ? { capabilities } : {},
|
|
492
|
-
...
|
|
449
|
+
...hooks ? { hooks } : {},
|
|
450
|
+
...connections ? { connections } : {},
|
|
451
|
+
...files ? { files } : {}
|
|
493
452
|
})
|
|
494
453
|
});
|
|
495
|
-
|
|
454
|
+
const fileCount = files ? Object.keys(files).length : 0;
|
|
455
|
+
console.log(`Pushed '${slug}' draft${fileCount > 0 ? ` (${fileCount} extra file${fileCount === 1 ? "" : "s"})` : ""}`);
|
|
496
456
|
}
|
|
497
457
|
function resolveSkillDir(slug) {
|
|
498
458
|
if (!slug) return process.cwd();
|
|
499
|
-
const installedPath =
|
|
459
|
+
const installedPath = path2.join(CLAUDE_SKILLS_DIR, slug);
|
|
500
460
|
if (existsSync(installedPath)) return installedPath;
|
|
501
|
-
return
|
|
461
|
+
return path2.join(process.cwd(), slug);
|
|
502
462
|
}
|
|
503
463
|
function registerPushCommand(program2) {
|
|
504
464
|
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) => {
|
|
505
465
|
requireAuth();
|
|
506
|
-
const resolvedSlug = slug ??
|
|
466
|
+
const resolvedSlug = slug ?? path2.basename(process.cwd());
|
|
507
467
|
const dir = opts.dir ?? resolveSkillDir(slug);
|
|
508
468
|
await pushSkill(resolvedSlug, dir);
|
|
509
469
|
if (opts.watch) {
|
|
@@ -525,6 +485,86 @@ function registerPushCommand(program2) {
|
|
|
525
485
|
});
|
|
526
486
|
}
|
|
527
487
|
|
|
488
|
+
// src/commands/skills/import.ts
|
|
489
|
+
var ORG_PROFILES = {
|
|
490
|
+
hippocratic: {
|
|
491
|
+
connectorHints: ["ehr.read", "ehr.write", "phi.access"],
|
|
492
|
+
fixtureTemplate: "patient-encounter",
|
|
493
|
+
channelPolicy: "stable"
|
|
494
|
+
},
|
|
495
|
+
revive: {
|
|
496
|
+
connectorHints: ["email.read", "calendar.read", "ads.read"],
|
|
497
|
+
fixtureTemplate: "campaign-brief",
|
|
498
|
+
channelPolicy: "beta"
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
function registerImportCommand(program2) {
|
|
502
|
+
program2.command("import <path>").description("Import a skill from a file, directory, or URL").option("--slug <slug>", "Override derived slug").option(
|
|
503
|
+
"--org <orgProfile>",
|
|
504
|
+
"Apply org-specific connector hints + fixture templates (hippocratic, revive)"
|
|
505
|
+
).action(async (inputPath, opts) => {
|
|
506
|
+
requireAuth();
|
|
507
|
+
let source;
|
|
508
|
+
let format = "auto";
|
|
509
|
+
let extraFiles = {};
|
|
510
|
+
try {
|
|
511
|
+
const stat2 = await fs3.stat(inputPath);
|
|
512
|
+
if (stat2.isDirectory()) {
|
|
513
|
+
const skillMdPath = path3.join(inputPath, "SKILL.md");
|
|
514
|
+
source = await fs3.readFile(skillMdPath, "utf-8");
|
|
515
|
+
format = "skill-md";
|
|
516
|
+
extraFiles = await collectExtraFiles(inputPath);
|
|
517
|
+
} else {
|
|
518
|
+
source = await fs3.readFile(inputPath, "utf-8");
|
|
519
|
+
if (inputPath.endsWith(".yaml") || inputPath.endsWith(".yml")) {
|
|
520
|
+
format = "headways-yaml";
|
|
521
|
+
} else {
|
|
522
|
+
format = "markdown";
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
} catch {
|
|
526
|
+
console.error(`Cannot read '${inputPath}': file or directory not found.`);
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
const profile = opts.org ? ORG_PROFILES[opts.org] : void 0;
|
|
530
|
+
if (opts.org && !profile) {
|
|
531
|
+
console.warn(
|
|
532
|
+
`Unknown org profile '${opts.org}'. Known profiles: ${Object.keys(ORG_PROFILES).join(", ")}`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
const derivedSlug = opts.slug ?? path3.basename(inputPath, path3.extname(inputPath)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
536
|
+
const result = await apiRequest("/v1/skills/import", {
|
|
537
|
+
method: "POST",
|
|
538
|
+
body: JSON.stringify({
|
|
539
|
+
source,
|
|
540
|
+
format,
|
|
541
|
+
suggestedSlug: derivedSlug,
|
|
542
|
+
connectorHints: profile?.connectorHints,
|
|
543
|
+
channelPolicy: profile?.channelPolicy,
|
|
544
|
+
...Object.keys(extraFiles).length > 0 ? { files: extraFiles } : {}
|
|
545
|
+
})
|
|
546
|
+
});
|
|
547
|
+
if (result.headline.length > 90) {
|
|
548
|
+
console.warn(
|
|
549
|
+
` Warning: headline is ${result.headline.length} chars (\u2264 90 required to publish). Edit headways.yaml and run 'headways skills push ${result.slug}'.`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
const fileCount = Object.keys(extraFiles).length;
|
|
553
|
+
console.log(`Imported as '${result.slug}' (format: ${result.detectedFormat})`);
|
|
554
|
+
console.log(` Headline: ${result.headline}`);
|
|
555
|
+
console.log(` Skill ID: ${result.skillId}`);
|
|
556
|
+
if (fileCount > 0) {
|
|
557
|
+
console.log(` Files: ${fileCount} extra file${fileCount === 1 ? "" : "s"} uploaded`);
|
|
558
|
+
}
|
|
559
|
+
if (profile) {
|
|
560
|
+
console.log(` Org profile: ${opts.org}`);
|
|
561
|
+
console.log(` Connector hints: ${profile.connectorHints.join(", ")}`);
|
|
562
|
+
console.log(` Fixture template: ${profile.fixtureTemplate}`);
|
|
563
|
+
}
|
|
564
|
+
console.log(` Run 'headways skills push ${result.slug}' to sync edits.`);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
528
568
|
// src/commands/skills/index.ts
|
|
529
569
|
var SKILLS_GUIDE = `
|
|
530
570
|
# Headways Skill Authoring Guide
|
|
@@ -544,12 +584,12 @@ headways skills push <slug> # push local edits as a draft
|
|
|
544
584
|
| Field | Rule |
|
|
545
585
|
|----------------|----------------------------------------------------------------|
|
|
546
586
|
| \`slug\` | \`^[a-z0-9-]+$\`, 1\u201364 chars, immutable after creation |
|
|
547
|
-
| \`headline\` |
|
|
587
|
+
| \`headline\` | **One short sentence, \u2264 90 chars** \u2014 hard gate at submit |
|
|
548
588
|
| \`name\` | 1\u2013120 chars (display name, defaults to headline) |
|
|
549
589
|
| \`channel\` | \`prompt\` (default) | \`auto\` | \`manual\` |
|
|
550
590
|
| \`data_classes\` | \`none\` (default) | \`pii\` | \`phi\` | \`pci\` |
|
|
551
591
|
|
|
552
|
-
> Critical: headline must be \u2264 90 characters
|
|
592
|
+
> Critical: headline must be one short sentence, \u2264 90 characters. The web UI blocks submission if exceeded.
|
|
553
593
|
|
|
554
594
|
## File Bundle (\`<slug>/\`)
|
|
555
595
|
|
|
@@ -584,7 +624,7 @@ edge cases, and concrete examples. Vague goals produce poor results.
|
|
|
584
624
|
\`\`\`yaml
|
|
585
625
|
slug: my-skill
|
|
586
626
|
name: My Skill
|
|
587
|
-
headline:
|
|
627
|
+
headline: One short sentence, verb-first (\u2264 90 chars)
|
|
588
628
|
channel: prompt # prompt | auto | manual
|
|
589
629
|
runtimes:
|
|
590
630
|
- claude-code
|
|
@@ -602,8 +642,8 @@ auto_send: false # true = skill may act without user confirmation
|
|
|
602
642
|
|
|
603
643
|
### connections.yaml (required for any skill that uses MCP connector tools)
|
|
604
644
|
|
|
605
|
-
Declare every MCP connector the skill depends on. Users see this list
|
|
606
|
-
|
|
645
|
+
Declare every MCP connector the skill depends on. Users see this list when they install the skill
|
|
646
|
+
via the Headways desktop app, which gates installation on the connectors being configured.
|
|
607
647
|
|
|
608
648
|
\`\`\`yaml
|
|
609
649
|
- connector: slack # connector identifier (e.g. slack, github, atlassian, linear, notion, google-drive, stripe, asana, hubspot, datadog)
|
|
@@ -614,6 +654,38 @@ and the Headways app gates installation on the connectors being configured.
|
|
|
614
654
|
|
|
615
655
|
Omit the file entirely if the skill has no connector dependencies.
|
|
616
656
|
|
|
657
|
+
## Runtime Events
|
|
658
|
+
|
|
659
|
+
Skills can emit events during execution that show up in the Headways dashboard.
|
|
660
|
+
Two layers fire events:
|
|
661
|
+
|
|
662
|
+
1. **Automatic** \u2014 Claude Code hooks emit \`tool.*\` (per tool call) and \`turn.end\`
|
|
663
|
+
(per response turn) for every skill invocation. A \`skill.start\`-style record is created
|
|
664
|
+
by the desktop app's \`UserPromptSubmit\` hook when the user invokes \`/<slug>\`. You don't write these.
|
|
665
|
+
|
|
666
|
+
2. **Manual** \u2014 Inside your skill body, call \`headways emit\` to record concrete
|
|
667
|
+
outcomes the agent produced.
|
|
668
|
+
|
|
669
|
+
\`\`\`bash
|
|
670
|
+
headways emit --hook outcome.pr_created -f url=https://github.com/...
|
|
671
|
+
headways emit --hook outcome.email_sent -f to=user@example.com
|
|
672
|
+
headways emit --hook outcome.doc_written -f path=wiki/pages/x.md
|
|
673
|
+
\`\`\`
|
|
674
|
+
|
|
675
|
+
### Hook name format
|
|
676
|
+
|
|
677
|
+
All hook names must match \`<namespace>.<name>\` where namespace is one of:
|
|
678
|
+
|
|
679
|
+
| Namespace | Emitted by | Example |
|
|
680
|
+
|-------------|---------------|--------------------------------------|
|
|
681
|
+
| \`skill.*\` | Hook (auto) | \`skill.start\` |
|
|
682
|
+
| \`tool.*\` | Hook (auto) | \`tool.Read\`, \`tool.Bash\` |
|
|
683
|
+
| \`turn.*\` | Hook (auto) | \`turn.end\` |
|
|
684
|
+
| \`outcome.*\` | Skill (manual)| \`outcome.pr_created\`, \`outcome.email_sent\` |
|
|
685
|
+
|
|
686
|
+
Names outside these four namespaces are rejected with HTTP 422. Pick a short,
|
|
687
|
+
verb-past-tense suffix for outcomes: \`pr_created\`, not \`creating_pr\`.
|
|
688
|
+
|
|
617
689
|
### hooks.yaml (omit if unused)
|
|
618
690
|
|
|
619
691
|
\`\`\`yaml
|
|
@@ -666,7 +738,6 @@ local edits with \`headways skills push <slug>\`.
|
|
|
666
738
|
|
|
667
739
|
## Common Failure Modes
|
|
668
740
|
|
|
669
|
-
- Headline > 90 chars \u2192 submit blocked with 422. Shorten before pushing.
|
|
670
741
|
- Uppercase or special chars in slug \u2192 rejected at creation. Use \`a-z\`, \`0-9\`, \`-\` only.
|
|
671
742
|
- Missing \`capabilities.yaml\` entries \u2192 skill silently blocked at runtime.
|
|
672
743
|
- 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.
|
|
@@ -681,8 +752,8 @@ function registerSkillsCommands(program2) {
|
|
|
681
752
|
console.log(SKILLS_GUIDE);
|
|
682
753
|
});
|
|
683
754
|
skills.command("list").description("List skills in the active org").action(async () => {
|
|
684
|
-
const { requireAuth: requireAuth2 } = await import("./config-
|
|
685
|
-
const { apiRequest: apiRequest2 } = await import("./api-
|
|
755
|
+
const { requireAuth: requireAuth2 } = await import("./config-APIR4RCR.js");
|
|
756
|
+
const { apiRequest: apiRequest2 } = await import("./api-H34ZX4FL.js");
|
|
686
757
|
requireAuth2();
|
|
687
758
|
const result = await apiRequest2("/v1/skills");
|
|
688
759
|
if (result.data.length === 0) {
|
|
@@ -693,27 +764,6 @@ function registerSkillsCommands(program2) {
|
|
|
693
764
|
}
|
|
694
765
|
}
|
|
695
766
|
});
|
|
696
|
-
skills.command("accept <slug>").description("Accept a pending skill update and install it locally").action(async (slug) => {
|
|
697
|
-
const { acceptSkill } = await import("./sync-Q3OQUWOD.js");
|
|
698
|
-
await acceptSkill(slug);
|
|
699
|
-
try {
|
|
700
|
-
const { apiRequest: apiRequest2 } = await import("./api-5EKGGFQ6.js");
|
|
701
|
-
const metadata = await apiRequest2(`/v1/skills/${slug}/bundle/metadata`);
|
|
702
|
-
const reqs = metadata.connectionRequirements ?? [];
|
|
703
|
-
if (reqs.length > 0) {
|
|
704
|
-
console.log("");
|
|
705
|
-
console.log("This skill requires the following connectors:");
|
|
706
|
-
console.log("");
|
|
707
|
-
for (const req of reqs) {
|
|
708
|
-
console.log(` - ${req.connector.padEnd(20)} ${req.purpose}`);
|
|
709
|
-
}
|
|
710
|
-
console.log("");
|
|
711
|
-
console.log("To authorize these connectors, use the Headways desktop app");
|
|
712
|
-
console.log("or run: headways connections add <provider>");
|
|
713
|
-
}
|
|
714
|
-
} catch {
|
|
715
|
-
}
|
|
716
|
-
});
|
|
717
767
|
skills.command("feedback <slug>").description("Submit feedback about a skill").option(
|
|
718
768
|
"--reaction <type>",
|
|
719
769
|
"thumbs_up, thumbs_down, wrong_output, missing_step",
|
|
@@ -762,14 +812,95 @@ function registerConnectionsCommands(program2) {
|
|
|
762
812
|
|
|
763
813
|
// src/sdk/emit.ts
|
|
764
814
|
import "commander";
|
|
815
|
+
|
|
816
|
+
// src/lib/run-session.ts
|
|
817
|
+
import {
|
|
818
|
+
existsSync as existsSync2,
|
|
819
|
+
mkdirSync,
|
|
820
|
+
readdirSync,
|
|
821
|
+
readFileSync,
|
|
822
|
+
statSync,
|
|
823
|
+
unlinkSync,
|
|
824
|
+
writeFileSync
|
|
825
|
+
} from "fs";
|
|
826
|
+
import { join as join4 } from "path";
|
|
827
|
+
var THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
828
|
+
async function readHookStdin() {
|
|
829
|
+
if (process.stdin.isTTY) return null;
|
|
830
|
+
const chunks = [];
|
|
831
|
+
for await (const chunk of process.stdin) {
|
|
832
|
+
chunks.push(chunk);
|
|
833
|
+
}
|
|
834
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
835
|
+
if (!raw) return null;
|
|
836
|
+
try {
|
|
837
|
+
return JSON.parse(raw);
|
|
838
|
+
} catch {
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
function sessionFilePath(sessionId) {
|
|
843
|
+
return join4(RUNS_DIR, sessionId);
|
|
844
|
+
}
|
|
845
|
+
function writeSessionRun(sessionId, run) {
|
|
846
|
+
if (!existsSync2(RUNS_DIR)) mkdirSync(RUNS_DIR, { recursive: true });
|
|
847
|
+
writeFileSync(sessionFilePath(sessionId), JSON.stringify(run));
|
|
848
|
+
}
|
|
849
|
+
function readSessionRun(sessionId) {
|
|
850
|
+
const path4 = sessionFilePath(sessionId);
|
|
851
|
+
if (!existsSync2(path4)) return null;
|
|
852
|
+
try {
|
|
853
|
+
return JSON.parse(readFileSync(path4, "utf8"));
|
|
854
|
+
} catch {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
function cleanupStaleRuns(now = Date.now()) {
|
|
859
|
+
if (!existsSync2(RUNS_DIR)) return;
|
|
860
|
+
let entries;
|
|
861
|
+
try {
|
|
862
|
+
entries = readdirSync(RUNS_DIR);
|
|
863
|
+
} catch {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
for (const name of entries) {
|
|
867
|
+
const path4 = join4(RUNS_DIR, name);
|
|
868
|
+
try {
|
|
869
|
+
const stat2 = statSync(path4);
|
|
870
|
+
if (now - stat2.mtimeMs > THIRTY_DAYS_MS) unlinkSync(path4);
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/sdk/emit.ts
|
|
877
|
+
var HOOK_NAME_RE = /^(skill|tool|turn|outcome)\.[A-Za-z0-9_]+$/;
|
|
878
|
+
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)";
|
|
765
879
|
function registerEmitCommand(program2) {
|
|
766
|
-
program2.command("emit").description(
|
|
880
|
+
program2.command("emit").description(
|
|
881
|
+
"Emit a skill run event (used by Claude Code hooks and from inside skills).\n" + HOOK_NAME_HELP
|
|
882
|
+
).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(
|
|
767
883
|
async (opts) => {
|
|
884
|
+
const hookName = opts.hook ?? opts.event ?? "tool.unknown";
|
|
885
|
+
if (!HOOK_NAME_RE.test(hookName)) {
|
|
886
|
+
console.error(`headways emit: invalid --hook "${hookName}"
|
|
887
|
+
|
|
888
|
+
${HOOK_NAME_HELP}`);
|
|
889
|
+
process.exitCode = 1;
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
768
892
|
const cfg = readConfig();
|
|
769
893
|
if (!cfg.token) return;
|
|
770
|
-
|
|
894
|
+
let runId = opts.runId ?? process.env["HEADWAYS_RUN_ID"];
|
|
895
|
+
if (!runId) {
|
|
896
|
+
const input = await readHookStdin();
|
|
897
|
+
const sessionId = input?.session_id;
|
|
898
|
+
if (typeof sessionId === "string") {
|
|
899
|
+
const session = readSessionRun(sessionId);
|
|
900
|
+
if (session) runId = session.runId;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
771
903
|
if (!runId) return;
|
|
772
|
-
const hookName = opts.hook ?? opts.event ?? "tool.unknown";
|
|
773
904
|
const payload = {};
|
|
774
905
|
if (opts.status !== void 0) {
|
|
775
906
|
payload["tool_result_is_error"] = opts.status === "true" || opts.status === "1";
|
|
@@ -800,8 +931,67 @@ function registerEmitCommand(program2) {
|
|
|
800
931
|
);
|
|
801
932
|
}
|
|
802
933
|
|
|
934
|
+
// src/commands/skill-run.ts
|
|
935
|
+
import { existsSync as existsSync3 } from "fs";
|
|
936
|
+
import { join as join5 } from "path";
|
|
937
|
+
import "commander";
|
|
938
|
+
var SLASH_COMMAND_RE = /^\/([a-z0-9-]+(?::[a-z0-9-]+)?)\b/;
|
|
939
|
+
function resolveSkillSlug(input, isInstalled) {
|
|
940
|
+
const direct = input.tool_input?.skill;
|
|
941
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
942
|
+
if (typeof input.prompt === "string") {
|
|
943
|
+
const m = SLASH_COMMAND_RE.exec(input.prompt);
|
|
944
|
+
if (m && m[1]) {
|
|
945
|
+
const slug = m[1];
|
|
946
|
+
if (isInstalled(slug)) return slug;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
function defaultIsInstalled(slug) {
|
|
952
|
+
return existsSync3(join5(INSTALLED_DIR, `${slug}.json`));
|
|
953
|
+
}
|
|
954
|
+
function registerSkillRunCommands(program2) {
|
|
955
|
+
const skillRun = program2.command("skill-run").description("Skill-run lifecycle commands (invoked by Claude Code hooks).");
|
|
956
|
+
skillRun.command("start").description(
|
|
957
|
+
"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>."
|
|
958
|
+
).action(async () => {
|
|
959
|
+
cleanupStaleRuns();
|
|
960
|
+
const cfg = readConfig();
|
|
961
|
+
if (!cfg.token) return;
|
|
962
|
+
const input = await readHookStdin();
|
|
963
|
+
if (!input) return;
|
|
964
|
+
const sessionId = input.session_id;
|
|
965
|
+
if (typeof sessionId !== "string") return;
|
|
966
|
+
const skillSlug = resolveSkillSlug(input, defaultIsInstalled);
|
|
967
|
+
if (!skillSlug) return;
|
|
968
|
+
let runId;
|
|
969
|
+
try {
|
|
970
|
+
const res = await fetch(`${getApiUrl()}/v1/skill-runs`, {
|
|
971
|
+
method: "POST",
|
|
972
|
+
headers: {
|
|
973
|
+
"Content-Type": "application/json",
|
|
974
|
+
Authorization: `Bearer ${cfg.token}`
|
|
975
|
+
},
|
|
976
|
+
body: JSON.stringify({ skillSlug, runtime: "claude-code" })
|
|
977
|
+
});
|
|
978
|
+
if (!res.ok) return;
|
|
979
|
+
const body = await res.json();
|
|
980
|
+
if (!body.runId) return;
|
|
981
|
+
runId = body.runId;
|
|
982
|
+
} catch {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
writeSessionRun(sessionId, {
|
|
986
|
+
runId,
|
|
987
|
+
skill: skillSlug,
|
|
988
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
|
|
803
993
|
// src/commands/prime.ts
|
|
804
|
-
import { existsSync as
|
|
994
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
805
995
|
import "commander";
|
|
806
996
|
function registerPrimeCommand(program2) {
|
|
807
997
|
program2.command("prime").description("Output Headways workflow context for AI coding assistants").action(() => {
|
|
@@ -817,6 +1007,11 @@ function registerPrimeCommand(program2) {
|
|
|
817
1007
|
"> `--help` to discover full options, or run `headways skills guide` before authoring a skill.",
|
|
818
1008
|
"> Do not guess at flags or constraints \u2014 discover them at runtime.",
|
|
819
1009
|
"",
|
|
1010
|
+
"## Responsibility split",
|
|
1011
|
+
"",
|
|
1012
|
+
"- **Desktop app (macOS):** installs skills, syncs the catalog, manages Claude Code hooks. Open it from `/Applications/Headways.app`.",
|
|
1013
|
+
"- **CLI (this tool):** skill *authoring* (push, import, new, list) plus runtime helpers Claude Code shells out to (`prime`, `emit`, `skill-run start`).",
|
|
1014
|
+
"",
|
|
820
1015
|
"## Auth & Config",
|
|
821
1016
|
"",
|
|
822
1017
|
`Status: ${cfg.token ? `Signed in (org: ${cfg.orgSlug ?? cfg.orgId ?? "unknown"})` : "Not signed in \u2014 run `headways login`"}`,
|
|
@@ -828,37 +1023,33 @@ function registerPrimeCommand(program2) {
|
|
|
828
1023
|
"headways login # Browser SSO sign-in",
|
|
829
1024
|
"headways logout # Remove stored credentials",
|
|
830
1025
|
"headways config status # Show saved key, org, URLs",
|
|
831
|
-
"headways config clear # Clear credentials
|
|
832
|
-
"",
|
|
833
|
-
"headways sync start # Pull catalog updates once",
|
|
834
|
-
"headways sync start --daemon # Poll every 60s in background",
|
|
835
|
-
"headways sync status # Show pending skill updates",
|
|
1026
|
+
"headways config clear # Clear credentials",
|
|
836
1027
|
"",
|
|
837
1028
|
"headways skills list # List skills in your org",
|
|
838
1029
|
"headways skills new # Scaffold a new skill",
|
|
839
1030
|
"headways skills import <path> # Create a new skill from a local file or directory",
|
|
840
1031
|
"headways skills push <slug> # Push edits to an existing skill (import or new first)",
|
|
841
|
-
"headways skills accept <slug> # Install a pending skill update",
|
|
842
1032
|
"headways skills feedback <slug> # Submit feedback on a skill",
|
|
843
1033
|
"headways skills guide # Authoring reference (run before creating a skill)",
|
|
844
1034
|
"",
|
|
845
|
-
"
|
|
846
|
-
"headways prime # Print this context
|
|
1035
|
+
"# Runtime helpers (invoked automatically by Claude Code hooks; rarely run by hand):",
|
|
1036
|
+
"headways prime # Print this context",
|
|
1037
|
+
"headways emit # Emit a skill-run event",
|
|
1038
|
+
"headways skill-run start # Start a skill run from a UserPromptSubmit hook",
|
|
847
1039
|
"```",
|
|
848
1040
|
"",
|
|
849
1041
|
"## Workflow",
|
|
850
1042
|
"",
|
|
851
|
-
"1.
|
|
852
|
-
"2.
|
|
853
|
-
"3.
|
|
854
|
-
"4. Run `headways sync start --daemon` to keep skills up to date in the background",
|
|
1043
|
+
"1. Open the **Headways desktop app** \u2014 it syncs the catalog, shows pending updates, and clicks-to-install bundles.",
|
|
1044
|
+
"2. New skill versions appear in the Library view; click **Install** to bring them onto this machine.",
|
|
1045
|
+
"3. To author skills: `headways skills new` or `headways skills import <path>`, edit, then `headways skills push <slug>`. Publish via the web UI.",
|
|
855
1046
|
"",
|
|
856
1047
|
"## Installed Skills",
|
|
857
1048
|
""
|
|
858
1049
|
];
|
|
859
1050
|
if (skills.length === 0) {
|
|
860
1051
|
lines.push(
|
|
861
|
-
"No skills installed.
|
|
1052
|
+
"No skills installed. Open the Headways desktop app to sync your org's catalog."
|
|
862
1053
|
);
|
|
863
1054
|
} else {
|
|
864
1055
|
for (const skill of skills) {
|
|
@@ -877,12 +1068,12 @@ function registerPrimeCommand(program2) {
|
|
|
877
1068
|
});
|
|
878
1069
|
}
|
|
879
1070
|
function getInstalledSkills() {
|
|
880
|
-
if (!
|
|
1071
|
+
if (!existsSync4(INSTALLED_DIR)) return [];
|
|
881
1072
|
try {
|
|
882
|
-
return
|
|
1073
|
+
return readdirSync2(INSTALLED_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
883
1074
|
const slug = f.replace(/\.json$/, "");
|
|
884
1075
|
try {
|
|
885
|
-
const raw = JSON.parse(
|
|
1076
|
+
const raw = JSON.parse(readFileSync2(`${INSTALLED_DIR}/${f}`, "utf8"));
|
|
886
1077
|
return {
|
|
887
1078
|
slug,
|
|
888
1079
|
version: String(raw.version ?? ""),
|
|
@@ -899,14 +1090,125 @@ function getInstalledSkills() {
|
|
|
899
1090
|
}
|
|
900
1091
|
}
|
|
901
1092
|
|
|
1093
|
+
// src/commands/upgrade.ts
|
|
1094
|
+
import { chmodSync, renameSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1095
|
+
import { delimiter, basename as basename3, dirname, join as join6 } from "path";
|
|
1096
|
+
import { existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
1097
|
+
import "commander";
|
|
1098
|
+
var GCS_BASE = "https://storage.googleapis.com/headways-releases/cli";
|
|
1099
|
+
var NPM_PACKAGE = "@headways/cli";
|
|
1100
|
+
function registerUpgradeCommand(program2) {
|
|
1101
|
+
program2.command("upgrade").description("Upgrade the Headways CLI to the latest version").option("--force", "Reinstall even if already on the latest version").action(async (opts) => {
|
|
1102
|
+
warnIfShadowed();
|
|
1103
|
+
const mode = detectInstallMode();
|
|
1104
|
+
if (mode.kind === "npm") {
|
|
1105
|
+
console.log(
|
|
1106
|
+
`This binary was installed via npm. Upgrade with:
|
|
1107
|
+
|
|
1108
|
+
npm i -g ${NPM_PACKAGE}@latest
|
|
1109
|
+
`
|
|
1110
|
+
);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (mode.kind === "unknown") {
|
|
1114
|
+
console.error(
|
|
1115
|
+
`Could not determine how this CLI was installed (execPath: ${process.execPath}).
|
|
1116
|
+
If installed via npm: npm i -g ${NPM_PACKAGE}@latest
|
|
1117
|
+
If installed via desktop: re-run the Install step from the Headways app.`
|
|
1118
|
+
);
|
|
1119
|
+
process.exitCode = 1;
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
await selfUpdate(mode.binPath, opts.force ?? false);
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
function detectInstallMode() {
|
|
1126
|
+
const exec = process.execPath;
|
|
1127
|
+
const execName = basename3(exec).toLowerCase();
|
|
1128
|
+
if (execName === "node" || execName === "bun" || execName.startsWith("node")) {
|
|
1129
|
+
return { kind: "npm" };
|
|
1130
|
+
}
|
|
1131
|
+
if (execName === "headways" || execName.startsWith("headways")) {
|
|
1132
|
+
return { kind: "standalone", binPath: exec };
|
|
1133
|
+
}
|
|
1134
|
+
return { kind: "unknown" };
|
|
1135
|
+
}
|
|
1136
|
+
function detectTriple() {
|
|
1137
|
+
if (process.platform !== "darwin") return null;
|
|
1138
|
+
if (process.arch === "arm64") return "aarch64-apple-darwin";
|
|
1139
|
+
if (process.arch === "x64") return "x86_64-apple-darwin";
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
async function selfUpdate(binPath, force) {
|
|
1143
|
+
const triple = detectTriple();
|
|
1144
|
+
if (!triple) {
|
|
1145
|
+
console.error(
|
|
1146
|
+
`No prebuilt binary for ${process.platform}/${process.arch}. Standalone binaries are macOS-only; on other platforms use: npm i -g ${NPM_PACKAGE}@latest`
|
|
1147
|
+
);
|
|
1148
|
+
process.exitCode = 1;
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
console.log(`Checking for updates\u2026 (current: ${"1.1.0"})`);
|
|
1152
|
+
const latestRes = await fetch(`${GCS_BASE}/latest.txt`);
|
|
1153
|
+
if (!latestRes.ok) {
|
|
1154
|
+
console.error(`Failed to fetch latest version: ${latestRes.status} ${latestRes.statusText}`);
|
|
1155
|
+
process.exitCode = 1;
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const latest = (await latestRes.text()).trim();
|
|
1159
|
+
if (latest === "1.1.0" && !force) {
|
|
1160
|
+
console.log(`Already on the latest version (${latest}).`);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
console.log(`Downloading ${latest} for ${triple}\u2026`);
|
|
1164
|
+
const binRes = await fetch(`${GCS_BASE}/${latest}/headways-${triple}`);
|
|
1165
|
+
if (!binRes.ok) {
|
|
1166
|
+
console.error(`Failed to download binary: ${binRes.status} ${binRes.statusText}`);
|
|
1167
|
+
process.exitCode = 1;
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const bin = Buffer.from(await binRes.arrayBuffer());
|
|
1171
|
+
const tmpPath = join6(dirname(binPath), `.headways.upgrade.${process.pid}`);
|
|
1172
|
+
writeFileSync2(tmpPath, bin);
|
|
1173
|
+
try {
|
|
1174
|
+
chmodSync(tmpPath, 493);
|
|
1175
|
+
renameSync(tmpPath, binPath);
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
try {
|
|
1178
|
+
unlinkSync2(tmpPath);
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
throw e;
|
|
1182
|
+
}
|
|
1183
|
+
console.log(`Upgraded to ${latest} \u2192 ${binPath}`);
|
|
1184
|
+
}
|
|
1185
|
+
function warnIfShadowed() {
|
|
1186
|
+
const exec = process.execPath;
|
|
1187
|
+
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
1188
|
+
const found = [];
|
|
1189
|
+
for (const dir of pathDirs) {
|
|
1190
|
+
const candidate = join6(dir, "headways");
|
|
1191
|
+
try {
|
|
1192
|
+
if (existsSync5(candidate) && statSync2(candidate).isFile()) {
|
|
1193
|
+
found.push(candidate);
|
|
1194
|
+
}
|
|
1195
|
+
} catch {
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (found.length > 0 && found[0] !== exec) {
|
|
1199
|
+
console.error(
|
|
1200
|
+
`note: another 'headways' is earlier on PATH at ${found[0]} \u2014 running upgrade here won't change which binary your shell picks.`
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
902
1205
|
// src/index.ts
|
|
903
|
-
program.name("headways").description("Headways CLI \u2014 skill authoring
|
|
1206
|
+
program.name("headways").description("Headways CLI \u2014 skill authoring + Claude Code runtime helpers").version("1.1.0");
|
|
904
1207
|
registerAuthCommands(program);
|
|
905
1208
|
registerSkillsCommands(program);
|
|
906
1209
|
registerConnectionsCommands(program);
|
|
907
|
-
registerSyncCommands(program);
|
|
908
1210
|
registerEmitCommand(program);
|
|
1211
|
+
registerSkillRunCommands(program);
|
|
909
1212
|
registerPrimeCommand(program);
|
|
910
|
-
|
|
911
|
-
registerUninstallCommand(program);
|
|
1213
|
+
registerUpgradeCommand(program);
|
|
912
1214
|
program.parse();
|