@headways/cli 0.4.2 → 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 +355 -141
- package/package.json +1 -1
- package/dist/chunk-XTEQBKIN.js +0 -409
- package/dist/sync-Q3OQUWOD.js +0 -16
package/README.md
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# @headways/cli
|
|
2
2
|
|
|
3
|
-
The official CLI for [Headways](https://headways.ai)
|
|
3
|
+
The official CLI for [Headways](https://headways.ai). Two responsibilities:
|
|
4
|
+
|
|
5
|
+
1. **Skill authoring** — talk to the backend API: scaffold, push drafts, list, submit feedback.
|
|
6
|
+
2. **Claude Code runtime helpers** — small binaries that Claude Code hooks shell out to (`prime`, `emit`, `skill-run start`).
|
|
7
|
+
|
|
8
|
+
Anything that mutates this machine's state — installing skill bundles, syncing the catalog, editing `~/.claude/settings.json` — lives in the [Headways desktop app](https://headways.ai/download) (macOS for now). The desktop auto-installs this CLI under `~/.local/bin/headways` on first launch.
|
|
4
9
|
|
|
5
10
|
## Installation
|
|
6
11
|
|
|
12
|
+
The desktop app installs the CLI for you. For dev / local work:
|
|
13
|
+
|
|
7
14
|
```bash
|
|
8
15
|
npm install -g @headways/cli
|
|
9
16
|
```
|
|
@@ -11,63 +18,66 @@ npm install -g @headways/cli
|
|
|
11
18
|
## Setup
|
|
12
19
|
|
|
13
20
|
```bash
|
|
14
|
-
headways
|
|
15
|
-
#
|
|
21
|
+
headways login
|
|
22
|
+
# Browser SSO. Saves credentials + org to ~/.headways/config.json.
|
|
16
23
|
```
|
|
17
24
|
|
|
18
|
-
Options:
|
|
19
|
-
|
|
20
25
|
```bash
|
|
21
|
-
headways
|
|
22
|
-
headways
|
|
23
|
-
headways configure status # show saved key + org
|
|
24
|
-
headways configure clear # remove credentials
|
|
26
|
+
headways config status # show saved token + org + URLs
|
|
27
|
+
headways config clear # remove credentials
|
|
25
28
|
```
|
|
26
29
|
|
|
27
30
|
## Commands
|
|
28
31
|
|
|
29
|
-
###
|
|
32
|
+
### Authoring
|
|
30
33
|
|
|
31
34
|
```bash
|
|
32
|
-
headways skills new # scaffold a new skill
|
|
35
|
+
headways skills new # scaffold a new skill (creates ./<slug>/)
|
|
33
36
|
headways skills new --slug <slug> --headline "<headline>" # non-interactive
|
|
34
|
-
headways skills import <path>
|
|
35
|
-
headways skills push <slug>
|
|
36
|
-
headways skills list
|
|
37
|
-
headways skills
|
|
37
|
+
headways skills import <path> # ingest an existing file or directory
|
|
38
|
+
headways skills push <slug> # push local edits as a draft
|
|
39
|
+
headways skills list # list skills in the active org
|
|
40
|
+
headways skills feedback <slug> # submit feedback on a skill
|
|
41
|
+
headways skills guide # authoring reference (run before creating a skill)
|
|
38
42
|
```
|
|
39
43
|
|
|
40
|
-
###
|
|
44
|
+
### Runtime helpers (invoked by Claude Code hooks; rarely run by hand)
|
|
41
45
|
|
|
42
46
|
```bash
|
|
43
|
-
headways
|
|
44
|
-
headways
|
|
45
|
-
headways
|
|
46
|
-
headways accept <skill> # install a pending update
|
|
47
|
+
headways prime # print workflow context for AI agents
|
|
48
|
+
headways emit --hook <namespace>.<name> # emit a skill-run event
|
|
49
|
+
headways skill-run start # create a run from a UserPromptSubmit hook
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
###
|
|
52
|
+
### Connections
|
|
50
53
|
|
|
51
54
|
```bash
|
|
52
|
-
headways
|
|
53
|
-
headways org use <slug> # switch active org
|
|
55
|
+
headways connections list # list MCP connector authorizations
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
## Creating a skill from scratch
|
|
57
59
|
|
|
58
60
|
```bash
|
|
59
61
|
cd ~/my-skills
|
|
60
|
-
headways skills new
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
#
|
|
66
|
-
headways skills push <slug>
|
|
67
|
-
# Publish via the web UI when ready
|
|
62
|
+
headways skills new # creates ./<slug>/ with SKILL.md,
|
|
63
|
+
# headways.yaml, capabilities.yaml,
|
|
64
|
+
# hooks.yaml
|
|
65
|
+
vim <slug>/SKILL.md # edit
|
|
66
|
+
headways skills push <slug> # push as draft
|
|
67
|
+
# Then publish via the web UI at app.headways.ai/skills/<slug>
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
Once published, the skill shows up in the desktop app's Library view with an **Install** button.
|
|
71
|
+
|
|
72
|
+
## Where things live
|
|
73
|
+
|
|
74
|
+
| Path | Purpose |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `~/.headways/config.json` | API token, org, URLs |
|
|
77
|
+
| `~/.headways/installed/<slug>.json` | Local install manifests (written by the desktop) |
|
|
78
|
+
| `~/.headways/runs/<session_id>` | Active run id per Claude Code session (written by `skill-run start`) |
|
|
79
|
+
| `~/.claude/skills/<slug>/` | Materialized skill bundles (written by the desktop) |
|
|
80
|
+
| `~/.claude/settings.json` | Claude Code hook wiring (written by the desktop) |
|
|
71
81
|
|
|
72
82
|
## Local development
|
|
73
83
|
|
|
@@ -8,6 +8,7 @@ var HEADWAYS_DIR = join(homedir(), ".headways");
|
|
|
8
8
|
var CONFIG_FILE = join(HEADWAYS_DIR, "config.json");
|
|
9
9
|
var CATALOG_FILE = join(HEADWAYS_DIR, "catalog.json");
|
|
10
10
|
var INSTALLED_DIR = join(HEADWAYS_DIR, "installed");
|
|
11
|
+
var RUNS_DIR = join(HEADWAYS_DIR, "runs");
|
|
11
12
|
var CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
12
13
|
function readConfig() {
|
|
13
14
|
if (!existsSync(CONFIG_FILE)) return {};
|
|
@@ -43,6 +44,7 @@ export {
|
|
|
43
44
|
CONFIG_FILE,
|
|
44
45
|
CATALOG_FILE,
|
|
45
46
|
INSTALLED_DIR,
|
|
47
|
+
RUNS_DIR,
|
|
46
48
|
CLAUDE_SKILLS_DIR,
|
|
47
49
|
readConfig,
|
|
48
50
|
writeConfig,
|
|
@@ -5,18 +5,20 @@ import {
|
|
|
5
5
|
CONFIG_FILE,
|
|
6
6
|
HEADWAYS_DIR,
|
|
7
7
|
INSTALLED_DIR,
|
|
8
|
+
RUNS_DIR,
|
|
8
9
|
getApiUrl,
|
|
9
10
|
getAppUrl,
|
|
10
11
|
readConfig,
|
|
11
12
|
requireAuth,
|
|
12
13
|
writeConfig
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-COGZMSYS.js";
|
|
14
15
|
export {
|
|
15
16
|
CATALOG_FILE,
|
|
16
17
|
CLAUDE_SKILLS_DIR,
|
|
17
18
|
CONFIG_FILE,
|
|
18
19
|
HEADWAYS_DIR,
|
|
19
20
|
INSTALLED_DIR,
|
|
21
|
+
RUNS_DIR,
|
|
20
22
|
getApiUrl,
|
|
21
23
|
getAppUrl,
|
|
22
24
|
readConfig,
|
package/dist/index.js
CHANGED
|
@@ -2,21 +2,17 @@
|
|
|
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";
|
|
@@ -25,7 +21,7 @@ import { program } from "commander";
|
|
|
25
21
|
// package.json
|
|
26
22
|
var package_default = {
|
|
27
23
|
name: "@headways/cli",
|
|
28
|
-
version: "0.
|
|
24
|
+
version: "1.0.0",
|
|
29
25
|
type: "module",
|
|
30
26
|
description: "Headways CLI \u2014 authoring, sync, and runtime SDK",
|
|
31
27
|
license: "MIT",
|
|
@@ -285,6 +281,12 @@ function registerNewCommand(program2) {
|
|
|
285
281
|
console.error("slug and headline are required.");
|
|
286
282
|
process.exit(1);
|
|
287
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
|
+
}
|
|
288
290
|
const dir = path.join(process.cwd(), slug);
|
|
289
291
|
await fs.mkdir(dir, { recursive: true });
|
|
290
292
|
await fs.writeFile(
|
|
@@ -356,96 +358,63 @@ runtimes: [claude-code]
|
|
|
356
358
|
|
|
357
359
|
// src/commands/skills/import.ts
|
|
358
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";
|
|
359
366
|
import * as fs2 from "fs/promises";
|
|
360
367
|
import * as path2 from "path";
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
program2.command("import <path>").description("Import a skill from a file, directory, or URL").option("--slug <slug>", "Override derived slug").option(
|
|
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";
|
|
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;
|
|
381
381
|
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
|
-
}
|
|
382
|
+
entries = await fs2.readdir(current, { withFileTypes: true });
|
|
395
383
|
} 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
|
-
);
|
|
384
|
+
return;
|
|
404
385
|
}
|
|
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}`);
|
|
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
|
+
}
|
|
423
396
|
}
|
|
424
|
-
|
|
425
|
-
|
|
397
|
+
}
|
|
398
|
+
await walk(dir, "");
|
|
399
|
+
return result;
|
|
426
400
|
}
|
|
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
401
|
var catchMissing = (e) => {
|
|
434
402
|
if (e.code === "ENOENT") return null;
|
|
435
403
|
throw e;
|
|
436
404
|
};
|
|
437
405
|
async function readSkillDir(dir) {
|
|
438
|
-
const skillMdPath =
|
|
406
|
+
const skillMdPath = path2.join(dir, "SKILL.md");
|
|
439
407
|
let body;
|
|
440
408
|
try {
|
|
441
|
-
body = await
|
|
409
|
+
body = await fs2.readFile(skillMdPath, "utf-8");
|
|
442
410
|
} catch {
|
|
443
411
|
throw new Error(`SKILL.md not found in ${dir}`);
|
|
444
412
|
}
|
|
445
|
-
const [headwaysYaml, capabilitiesYaml, connectionsYaml] = await Promise.all([
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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)
|
|
449
418
|
]);
|
|
450
419
|
let headline;
|
|
451
420
|
if (headwaysYaml) {
|
|
@@ -457,7 +426,15 @@ async function readSkillDir(dir) {
|
|
|
457
426
|
const items = parseConnectionsYaml(connectionsYaml);
|
|
458
427
|
if (items.length > 0) connections = items;
|
|
459
428
|
}
|
|
460
|
-
|
|
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
|
+
};
|
|
461
438
|
}
|
|
462
439
|
function parseConnectionsYaml(yaml) {
|
|
463
440
|
const items = [];
|
|
@@ -482,28 +459,36 @@ function parseConnectionsYaml(yaml) {
|
|
|
482
459
|
return items;
|
|
483
460
|
}
|
|
484
461
|
async function pushSkill(slug, dir) {
|
|
485
|
-
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
|
+
}
|
|
486
468
|
await apiRequest(`/v1/skills/${slug}/draft`, {
|
|
487
469
|
method: "PUT",
|
|
488
470
|
body: JSON.stringify({
|
|
489
471
|
body,
|
|
490
472
|
...headline ? { headline } : {},
|
|
491
473
|
...capabilities ? { capabilities } : {},
|
|
492
|
-
...
|
|
474
|
+
...hooks ? { hooks } : {},
|
|
475
|
+
...connections ? { connections } : {},
|
|
476
|
+
...files ? { files } : {}
|
|
493
477
|
})
|
|
494
478
|
});
|
|
495
|
-
|
|
479
|
+
const fileCount = files ? Object.keys(files).length : 0;
|
|
480
|
+
console.log(`Pushed '${slug}' draft${fileCount > 0 ? ` (${fileCount} extra file${fileCount === 1 ? "" : "s"})` : ""}`);
|
|
496
481
|
}
|
|
497
482
|
function resolveSkillDir(slug) {
|
|
498
483
|
if (!slug) return process.cwd();
|
|
499
|
-
const installedPath =
|
|
484
|
+
const installedPath = path2.join(CLAUDE_SKILLS_DIR, slug);
|
|
500
485
|
if (existsSync(installedPath)) return installedPath;
|
|
501
|
-
return
|
|
486
|
+
return path2.join(process.cwd(), slug);
|
|
502
487
|
}
|
|
503
488
|
function registerPushCommand(program2) {
|
|
504
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) => {
|
|
505
490
|
requireAuth();
|
|
506
|
-
const resolvedSlug = slug ??
|
|
491
|
+
const resolvedSlug = slug ?? path2.basename(process.cwd());
|
|
507
492
|
const dir = opts.dir ?? resolveSkillDir(slug);
|
|
508
493
|
await pushSkill(resolvedSlug, dir);
|
|
509
494
|
if (opts.watch) {
|
|
@@ -525,6 +510,86 @@ function registerPushCommand(program2) {
|
|
|
525
510
|
});
|
|
526
511
|
}
|
|
527
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
|
+
|
|
528
593
|
// src/commands/skills/index.ts
|
|
529
594
|
var SKILLS_GUIDE = `
|
|
530
595
|
# Headways Skill Authoring Guide
|
|
@@ -544,12 +609,12 @@ headways skills push <slug> # push local edits as a draft
|
|
|
544
609
|
| Field | Rule |
|
|
545
610
|
|----------------|----------------------------------------------------------------|
|
|
546
611
|
| \`slug\` | \`^[a-z0-9-]+$\`, 1\u201364 chars, immutable after creation |
|
|
547
|
-
| \`headline\` |
|
|
612
|
+
| \`headline\` | **One short sentence, \u2264 90 chars** \u2014 hard gate at submit |
|
|
548
613
|
| \`name\` | 1\u2013120 chars (display name, defaults to headline) |
|
|
549
614
|
| \`channel\` | \`prompt\` (default) | \`auto\` | \`manual\` |
|
|
550
615
|
| \`data_classes\` | \`none\` (default) | \`pii\` | \`phi\` | \`pci\` |
|
|
551
616
|
|
|
552
|
-
> 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.
|
|
553
618
|
|
|
554
619
|
## File Bundle (\`<slug>/\`)
|
|
555
620
|
|
|
@@ -584,7 +649,7 @@ edge cases, and concrete examples. Vague goals produce poor results.
|
|
|
584
649
|
\`\`\`yaml
|
|
585
650
|
slug: my-skill
|
|
586
651
|
name: My Skill
|
|
587
|
-
headline:
|
|
652
|
+
headline: One short sentence, verb-first (\u2264 90 chars)
|
|
588
653
|
channel: prompt # prompt | auto | manual
|
|
589
654
|
runtimes:
|
|
590
655
|
- claude-code
|
|
@@ -602,8 +667,8 @@ auto_send: false # true = skill may act without user confirmation
|
|
|
602
667
|
|
|
603
668
|
### connections.yaml (required for any skill that uses MCP connector tools)
|
|
604
669
|
|
|
605
|
-
Declare every MCP connector the skill depends on. Users see this list
|
|
606
|
-
|
|
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.
|
|
607
672
|
|
|
608
673
|
\`\`\`yaml
|
|
609
674
|
- connector: slack # connector identifier (e.g. slack, github, atlassian, linear, notion, google-drive, stripe, asana, hubspot, datadog)
|
|
@@ -614,6 +679,38 @@ and the Headways app gates installation on the connectors being configured.
|
|
|
614
679
|
|
|
615
680
|
Omit the file entirely if the skill has no connector dependencies.
|
|
616
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
|
+
|
|
617
714
|
### hooks.yaml (omit if unused)
|
|
618
715
|
|
|
619
716
|
\`\`\`yaml
|
|
@@ -666,7 +763,6 @@ local edits with \`headways skills push <slug>\`.
|
|
|
666
763
|
|
|
667
764
|
## Common Failure Modes
|
|
668
765
|
|
|
669
|
-
- Headline > 90 chars \u2192 submit blocked with 422. Shorten before pushing.
|
|
670
766
|
- Uppercase or special chars in slug \u2192 rejected at creation. Use \`a-z\`, \`0-9\`, \`-\` only.
|
|
671
767
|
- Missing \`capabilities.yaml\` entries \u2192 skill silently blocked at runtime.
|
|
672
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.
|
|
@@ -681,8 +777,8 @@ function registerSkillsCommands(program2) {
|
|
|
681
777
|
console.log(SKILLS_GUIDE);
|
|
682
778
|
});
|
|
683
779
|
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-
|
|
780
|
+
const { requireAuth: requireAuth2 } = await import("./config-APIR4RCR.js");
|
|
781
|
+
const { apiRequest: apiRequest2 } = await import("./api-H34ZX4FL.js");
|
|
686
782
|
requireAuth2();
|
|
687
783
|
const result = await apiRequest2("/v1/skills");
|
|
688
784
|
if (result.data.length === 0) {
|
|
@@ -693,27 +789,6 @@ function registerSkillsCommands(program2) {
|
|
|
693
789
|
}
|
|
694
790
|
}
|
|
695
791
|
});
|
|
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
792
|
skills.command("feedback <slug>").description("Submit feedback about a skill").option(
|
|
718
793
|
"--reaction <type>",
|
|
719
794
|
"thumbs_up, thumbs_down, wrong_output, missing_step",
|
|
@@ -762,14 +837,95 @@ function registerConnectionsCommands(program2) {
|
|
|
762
837
|
|
|
763
838
|
// src/sdk/emit.ts
|
|
764
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)";
|
|
765
904
|
function registerEmitCommand(program2) {
|
|
766
|
-
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(
|
|
767
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
|
+
}
|
|
768
917
|
const cfg = readConfig();
|
|
769
918
|
if (!cfg.token) return;
|
|
770
|
-
|
|
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
|
+
}
|
|
771
928
|
if (!runId) return;
|
|
772
|
-
const hookName = opts.hook ?? opts.event ?? "tool.unknown";
|
|
773
929
|
const payload = {};
|
|
774
930
|
if (opts.status !== void 0) {
|
|
775
931
|
payload["tool_result_is_error"] = opts.status === "true" || opts.status === "1";
|
|
@@ -800,8 +956,67 @@ function registerEmitCommand(program2) {
|
|
|
800
956
|
);
|
|
801
957
|
}
|
|
802
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
|
+
|
|
803
1018
|
// src/commands/prime.ts
|
|
804
|
-
import { existsSync as
|
|
1019
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
805
1020
|
import "commander";
|
|
806
1021
|
function registerPrimeCommand(program2) {
|
|
807
1022
|
program2.command("prime").description("Output Headways workflow context for AI coding assistants").action(() => {
|
|
@@ -817,6 +1032,11 @@ function registerPrimeCommand(program2) {
|
|
|
817
1032
|
"> `--help` to discover full options, or run `headways skills guide` before authoring a skill.",
|
|
818
1033
|
"> Do not guess at flags or constraints \u2014 discover them at runtime.",
|
|
819
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
|
+
"",
|
|
820
1040
|
"## Auth & Config",
|
|
821
1041
|
"",
|
|
822
1042
|
`Status: ${cfg.token ? `Signed in (org: ${cfg.orgSlug ?? cfg.orgId ?? "unknown"})` : "Not signed in \u2014 run `headways login`"}`,
|
|
@@ -828,37 +1048,33 @@ function registerPrimeCommand(program2) {
|
|
|
828
1048
|
"headways login # Browser SSO sign-in",
|
|
829
1049
|
"headways logout # Remove stored credentials",
|
|
830
1050
|
"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",
|
|
1051
|
+
"headways config clear # Clear credentials",
|
|
836
1052
|
"",
|
|
837
1053
|
"headways skills list # List skills in your org",
|
|
838
1054
|
"headways skills new # Scaffold a new skill",
|
|
839
1055
|
"headways skills import <path> # Create a new skill from a local file or directory",
|
|
840
1056
|
"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
1057
|
"headways skills feedback <slug> # Submit feedback on a skill",
|
|
843
1058
|
"headways skills guide # Authoring reference (run before creating a skill)",
|
|
844
1059
|
"",
|
|
845
|
-
"
|
|
846
|
-
"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",
|
|
847
1064
|
"```",
|
|
848
1065
|
"",
|
|
849
1066
|
"## Workflow",
|
|
850
1067
|
"",
|
|
851
|
-
"1.
|
|
852
|
-
"2.
|
|
853
|
-
"3.
|
|
854
|
-
"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.",
|
|
855
1071
|
"",
|
|
856
1072
|
"## Installed Skills",
|
|
857
1073
|
""
|
|
858
1074
|
];
|
|
859
1075
|
if (skills.length === 0) {
|
|
860
1076
|
lines.push(
|
|
861
|
-
"No skills installed.
|
|
1077
|
+
"No skills installed. Open the Headways desktop app to sync your org's catalog."
|
|
862
1078
|
);
|
|
863
1079
|
} else {
|
|
864
1080
|
for (const skill of skills) {
|
|
@@ -877,12 +1093,12 @@ function registerPrimeCommand(program2) {
|
|
|
877
1093
|
});
|
|
878
1094
|
}
|
|
879
1095
|
function getInstalledSkills() {
|
|
880
|
-
if (!
|
|
1096
|
+
if (!existsSync4(INSTALLED_DIR)) return [];
|
|
881
1097
|
try {
|
|
882
|
-
return
|
|
1098
|
+
return readdirSync2(INSTALLED_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
883
1099
|
const slug = f.replace(/\.json$/, "");
|
|
884
1100
|
try {
|
|
885
|
-
const raw = JSON.parse(
|
|
1101
|
+
const raw = JSON.parse(readFileSync2(`${INSTALLED_DIR}/${f}`, "utf8"));
|
|
886
1102
|
return {
|
|
887
1103
|
slug,
|
|
888
1104
|
version: String(raw.version ?? ""),
|
|
@@ -900,13 +1116,11 @@ function getInstalledSkills() {
|
|
|
900
1116
|
}
|
|
901
1117
|
|
|
902
1118
|
// src/index.ts
|
|
903
|
-
program.name("headways").description("Headways CLI \u2014 skill authoring
|
|
1119
|
+
program.name("headways").description("Headways CLI \u2014 skill authoring + Claude Code runtime helpers").version(package_default.version);
|
|
904
1120
|
registerAuthCommands(program);
|
|
905
1121
|
registerSkillsCommands(program);
|
|
906
1122
|
registerConnectionsCommands(program);
|
|
907
|
-
registerSyncCommands(program);
|
|
908
1123
|
registerEmitCommand(program);
|
|
1124
|
+
registerSkillRunCommands(program);
|
|
909
1125
|
registerPrimeCommand(program);
|
|
910
|
-
registerSetupCommand(program);
|
|
911
|
-
registerUninstallCommand(program);
|
|
912
1126
|
program.parse();
|
package/package.json
CHANGED
package/dist/chunk-XTEQBKIN.js
DELETED
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
CLAUDE_SKILLS_DIR,
|
|
4
|
-
HEADWAYS_DIR,
|
|
5
|
-
getApiUrl,
|
|
6
|
-
readConfig
|
|
7
|
-
} from "./chunk-UUFIIGTZ.js";
|
|
8
|
-
|
|
9
|
-
// src/commands/sync/index.ts
|
|
10
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, rmSync as rmSync2 } from "fs";
|
|
11
|
-
import { join as join2 } from "path";
|
|
12
|
-
import { createGunzip } from "zlib";
|
|
13
|
-
import { Readable } from "stream";
|
|
14
|
-
import "stream/promises";
|
|
15
|
-
import "commander";
|
|
16
|
-
|
|
17
|
-
// src/commands/setup.ts
|
|
18
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
19
|
-
import { homedir } from "os";
|
|
20
|
-
import { dirname, join } from "path";
|
|
21
|
-
import "commander";
|
|
22
|
-
var CLAUDE_SETTINGS_GLOBAL = join(homedir(), ".claude", "settings.json");
|
|
23
|
-
var CLAUDE_SETTINGS_LOCAL = join(".claude", "settings.json");
|
|
24
|
-
var PRIME_HOOK = { type: "command", command: "headways prime" };
|
|
25
|
-
var CLAUDE_MD_SECTION = `
|
|
26
|
-
<!-- BEGIN HEADWAYS -->
|
|
27
|
-
## Headways Skills
|
|
28
|
-
|
|
29
|
-
This project uses [Headways](https://headways.ai) for AI skill management.
|
|
30
|
-
|
|
31
|
-
- \`headways prime\` \u2014 load workflow context (called automatically by hooks)
|
|
32
|
-
- \`headways sync start\` \u2014 pull latest skill updates from your org
|
|
33
|
-
- \`headways accept <skill>\` \u2014 install a pending skill update
|
|
34
|
-
- \`headways skills list\` \u2014 list skills in your org
|
|
35
|
-
|
|
36
|
-
Skills are installed to \`~/.claude/skills/<slug>/\` and loaded automatically by Claude Code.
|
|
37
|
-
<!-- END HEADWAYS -->
|
|
38
|
-
`.trimStart();
|
|
39
|
-
function registerUninstallCommand(program) {
|
|
40
|
-
program.command("uninstall").description("Remove all Headways hooks, skill files, and local state").option("--yes", "Skip confirmation prompt").action(async (opts) => {
|
|
41
|
-
const { createInterface } = await import("readline/promises");
|
|
42
|
-
if (!opts.yes) {
|
|
43
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
44
|
-
const ans = await rl.question(
|
|
45
|
-
"This will remove all local Headways data (hooks, skill files, config). Continue? [y/N] "
|
|
46
|
-
);
|
|
47
|
-
rl.close();
|
|
48
|
-
if (ans.trim().toLowerCase() !== "y") {
|
|
49
|
-
console.log("Aborted.");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
removeHooks(CLAUDE_SETTINGS_GLOBAL);
|
|
54
|
-
removeHooks(CLAUDE_SETTINGS_LOCAL);
|
|
55
|
-
console.log("\u2713 Removed Claude Code hooks");
|
|
56
|
-
if (existsSync(CLAUDE_SKILLS_DIR)) {
|
|
57
|
-
rmSync(CLAUDE_SKILLS_DIR, { recursive: true, force: true });
|
|
58
|
-
console.log("\u2713 Removed ~/.claude/skills/");
|
|
59
|
-
}
|
|
60
|
-
const headwaysDir = join(homedir(), ".headways");
|
|
61
|
-
if (existsSync(headwaysDir)) {
|
|
62
|
-
rmSync(headwaysDir, { recursive: true, force: true });
|
|
63
|
-
console.log("\u2713 Removed ~/.headways/");
|
|
64
|
-
}
|
|
65
|
-
console.log(
|
|
66
|
-
"\nHeadways data removed. You can now delete the Headways.app from /Applications."
|
|
67
|
-
);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
function registerSetupCommand(program) {
|
|
71
|
-
const setup = program.command("setup").description("Install Headways integration for AI coding assistants");
|
|
72
|
-
setup.command("claude").description("Install Claude Code hooks (SessionStart + PreCompact)").option("--project", "Install for this project only (default: global)").option("--remove", "Remove the integration").option("--check", "Check if integration is installed").action((opts) => {
|
|
73
|
-
const settingsPath = opts.project ? CLAUDE_SETTINGS_LOCAL : CLAUDE_SETTINGS_GLOBAL;
|
|
74
|
-
if (opts.check) {
|
|
75
|
-
const installed = isInstalled(settingsPath);
|
|
76
|
-
console.log(
|
|
77
|
-
installed ? `\u2713 Installed: ${settingsPath}` : `\u2717 Not installed: ${settingsPath}`
|
|
78
|
-
);
|
|
79
|
-
process.exit(installed ? 0 : 1);
|
|
80
|
-
}
|
|
81
|
-
if (opts.remove) {
|
|
82
|
-
removeHooks(settingsPath);
|
|
83
|
-
console.log(`Removed Headways hooks from ${settingsPath}`);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
installHooks(settingsPath, opts.project ?? false);
|
|
87
|
-
updateClaudeMd(opts.project ?? false);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
function ensureClaudeHooks() {
|
|
91
|
-
if (!isInstalled(CLAUDE_SETTINGS_GLOBAL)) {
|
|
92
|
-
installHooks(CLAUDE_SETTINGS_GLOBAL, false, true);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function readSettings(path) {
|
|
96
|
-
if (!existsSync(path)) return {};
|
|
97
|
-
try {
|
|
98
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
99
|
-
} catch {
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function writeSettings(path, settings) {
|
|
104
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
105
|
-
writeFileSync(path, JSON.stringify(settings, null, 2) + "\n");
|
|
106
|
-
}
|
|
107
|
-
function isInstalled(settingsPath) {
|
|
108
|
-
const s = readSettings(settingsPath);
|
|
109
|
-
const hooks = s["hooks"];
|
|
110
|
-
if (!hooks) return false;
|
|
111
|
-
return ["SessionStart", "PreCompact"].every(
|
|
112
|
-
(event) => hooks[event]?.some(
|
|
113
|
-
(entry) => entry.hooks?.some((h) => h.command === PRIME_HOOK.command)
|
|
114
|
-
)
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
function installHooks(settingsPath, isProject, silent = false) {
|
|
118
|
-
const settings = readSettings(settingsPath);
|
|
119
|
-
const hooks = settings["hooks"] ?? {};
|
|
120
|
-
for (const event of ["SessionStart", "PreCompact"]) {
|
|
121
|
-
const entries = hooks[event] ?? [];
|
|
122
|
-
for (const entry of entries) {
|
|
123
|
-
entry.hooks = entry.hooks.filter((h) => h.command !== PRIME_HOOK.command);
|
|
124
|
-
}
|
|
125
|
-
const mainEntry = entries.find((e) => e.matcher === "" || e.matcher === void 0);
|
|
126
|
-
if (mainEntry) {
|
|
127
|
-
mainEntry.hooks.push(PRIME_HOOK);
|
|
128
|
-
} else {
|
|
129
|
-
entries.unshift({ matcher: "", hooks: [PRIME_HOOK] });
|
|
130
|
-
}
|
|
131
|
-
hooks[event] = entries;
|
|
132
|
-
}
|
|
133
|
-
settings["hooks"] = hooks;
|
|
134
|
-
writeSettings(settingsPath, settings);
|
|
135
|
-
if (!silent) {
|
|
136
|
-
console.log(`\u2713 Hooks installed: ${settingsPath}`);
|
|
137
|
-
if (!isProject) {
|
|
138
|
-
console.log(" SessionStart + PreCompact \u2192 headways prime");
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
function removeHooks(settingsPath) {
|
|
143
|
-
if (!existsSync(settingsPath)) return;
|
|
144
|
-
const settings = readSettings(settingsPath);
|
|
145
|
-
const hooks = settings["hooks"] ?? {};
|
|
146
|
-
for (const event of ["SessionStart", "PreCompact"]) {
|
|
147
|
-
if (hooks[event]) {
|
|
148
|
-
for (const entry of hooks[event]) {
|
|
149
|
-
entry.hooks = entry.hooks.filter((h) => h.command !== PRIME_HOOK.command);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
settings["hooks"] = hooks;
|
|
154
|
-
writeSettings(settingsPath, settings);
|
|
155
|
-
}
|
|
156
|
-
function updateClaudeMd(isProject) {
|
|
157
|
-
const claudeMdPath = isProject ? "CLAUDE.md" : join(homedir(), "CLAUDE.md");
|
|
158
|
-
if (!existsSync(claudeMdPath)) return;
|
|
159
|
-
const content = readFileSync(claudeMdPath, "utf8");
|
|
160
|
-
if (content.includes("<!-- BEGIN HEADWAYS -->")) return;
|
|
161
|
-
writeFileSync(claudeMdPath, content.trimEnd() + "\n\n" + CLAUDE_MD_SECTION);
|
|
162
|
-
console.log(`\u2713 Updated ${claudeMdPath}`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// src/commands/sync/index.ts
|
|
166
|
-
var PENDING_FILE = join2(HEADWAYS_DIR, "pending.json");
|
|
167
|
-
var SYNC_STATE_FILE = join2(HEADWAYS_DIR, "sync-state.json");
|
|
168
|
-
var INSTALLED_DIR = join2(HEADWAYS_DIR, "installed");
|
|
169
|
-
function readSyncState() {
|
|
170
|
-
if (!existsSync2(SYNC_STATE_FILE)) return {};
|
|
171
|
-
try {
|
|
172
|
-
return JSON.parse(readFileSync2(SYNC_STATE_FILE, "utf8"));
|
|
173
|
-
} catch {
|
|
174
|
-
return {};
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
function writeSyncState(state) {
|
|
178
|
-
if (!existsSync2(HEADWAYS_DIR)) mkdirSync2(HEADWAYS_DIR, { recursive: true });
|
|
179
|
-
writeFileSync2(SYNC_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
180
|
-
}
|
|
181
|
-
function readPending() {
|
|
182
|
-
if (!existsSync2(PENDING_FILE)) return [];
|
|
183
|
-
try {
|
|
184
|
-
return JSON.parse(readFileSync2(PENDING_FILE, "utf8"));
|
|
185
|
-
} catch {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function writePending(updates) {
|
|
190
|
-
if (!existsSync2(HEADWAYS_DIR)) mkdirSync2(HEADWAYS_DIR, { recursive: true });
|
|
191
|
-
writeFileSync2(PENDING_FILE, JSON.stringify(updates, null, 2) + "\n");
|
|
192
|
-
}
|
|
193
|
-
function deviceHeaders(state) {
|
|
194
|
-
return {
|
|
195
|
-
Authorization: `Bearer ${state.device_token ?? ""}`,
|
|
196
|
-
"x-headways-device-id": state.device_id ?? "",
|
|
197
|
-
"x-headways-timestamp": String(Math.floor(Date.now() / 1e3))
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
async function registerDevice(token, orgId, apiUrl) {
|
|
201
|
-
const existingId = readSyncState().device_id;
|
|
202
|
-
const pubKey = existingId ? Buffer.from(`desktop-${existingId}`).toString("base64url") : Buffer.from(`desktop-${Date.now()}-${Math.random()}`).toString("base64url");
|
|
203
|
-
const res = await fetch(`${apiUrl}/v1/sync/devices/register`, {
|
|
204
|
-
method: "POST",
|
|
205
|
-
headers: {
|
|
206
|
-
"Content-Type": "application/json",
|
|
207
|
-
Authorization: `Bearer ${token}`,
|
|
208
|
-
"x-headways-org-id": orgId
|
|
209
|
-
},
|
|
210
|
-
body: JSON.stringify({
|
|
211
|
-
publicKey: pubKey,
|
|
212
|
-
platform: process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux",
|
|
213
|
-
hostname: (await import("os")).hostname()
|
|
214
|
-
})
|
|
215
|
-
});
|
|
216
|
-
if (!res.ok) throw new Error(`Device registration failed: ${res.status}`);
|
|
217
|
-
const data = await res.json();
|
|
218
|
-
return { device_id: data.deviceId, device_token: data.deviceToken };
|
|
219
|
-
}
|
|
220
|
-
async function pollCatalog(state, apiUrl) {
|
|
221
|
-
const url = state.etag ? `${apiUrl}/v1/sync/catalog?since=${encodeURIComponent(state.etag)}` : `${apiUrl}/v1/sync/catalog`;
|
|
222
|
-
const res = await fetch(url, { headers: deviceHeaders(state) });
|
|
223
|
-
if (res.status === 304) return null;
|
|
224
|
-
if (!res.ok) throw new Error(`Catalog poll failed: ${res.status}`);
|
|
225
|
-
return res.json();
|
|
226
|
-
}
|
|
227
|
-
async function downloadAndMaterialize(slug, version, state, apiUrl) {
|
|
228
|
-
const res = await fetch(`${apiUrl}/v1/sync/bundles/${slug}/${version}`, {
|
|
229
|
-
redirect: "follow",
|
|
230
|
-
headers: deviceHeaders(state)
|
|
231
|
-
});
|
|
232
|
-
if (!res.ok) throw new Error(`Bundle fetch failed: ${res.status}`);
|
|
233
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
234
|
-
const dest = join2(CLAUDE_SKILLS_DIR, slug);
|
|
235
|
-
const staging = join2(CLAUDE_SKILLS_DIR, `.${slug}-staging`);
|
|
236
|
-
mkdirSync2(staging, { recursive: true });
|
|
237
|
-
await extractTarGz(buf, staging);
|
|
238
|
-
if (existsSync2(dest)) rmSync2(dest, { recursive: true });
|
|
239
|
-
renameSync(staging, dest);
|
|
240
|
-
mkdirSync2(INSTALLED_DIR, { recursive: true });
|
|
241
|
-
writeFileSync2(
|
|
242
|
-
join2(INSTALLED_DIR, `${slug}.json`),
|
|
243
|
-
JSON.stringify(
|
|
244
|
-
{ slug, version, runtime: "claude-code", installed_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
245
|
-
null,
|
|
246
|
-
2
|
|
247
|
-
)
|
|
248
|
-
);
|
|
249
|
-
console.log(`Materialized ${slug}@${version} \u2192 ${dest}`);
|
|
250
|
-
}
|
|
251
|
-
async function extractTarGz(buf, destDir) {
|
|
252
|
-
const decompressed = await new Promise((resolve, reject) => {
|
|
253
|
-
const chunks = [];
|
|
254
|
-
const gunzip = createGunzip();
|
|
255
|
-
const src = Readable.from(buf);
|
|
256
|
-
src.pipe(gunzip);
|
|
257
|
-
gunzip.on("data", (chunk) => chunks.push(chunk));
|
|
258
|
-
gunzip.on("end", () => resolve(Buffer.concat(chunks)));
|
|
259
|
-
gunzip.on("error", reject);
|
|
260
|
-
});
|
|
261
|
-
let offset = 0;
|
|
262
|
-
const { writeFileSync: wf, mkdirSync: md } = await import("fs");
|
|
263
|
-
const { dirname: dirname2 } = await import("path");
|
|
264
|
-
while (offset + 512 <= decompressed.length) {
|
|
265
|
-
const header = decompressed.slice(offset, offset + 512);
|
|
266
|
-
const name = header.slice(0, 100).toString("utf8").replace(/\0/g, "").trim();
|
|
267
|
-
if (!name) break;
|
|
268
|
-
const sizeOctal = header.slice(124, 136).toString("utf8").replace(/\0/g, "").trim();
|
|
269
|
-
const size = parseInt(sizeOctal, 8) || 0;
|
|
270
|
-
const typeFlag = header[156];
|
|
271
|
-
offset += 512;
|
|
272
|
-
if (typeFlag === 53 || name.endsWith("/")) {
|
|
273
|
-
md(join2(destDir, name), { recursive: true });
|
|
274
|
-
} else if (typeFlag === 0 || typeFlag === 48 || typeFlag === void 0) {
|
|
275
|
-
const filePath = join2(destDir, name);
|
|
276
|
-
md(dirname2(filePath), { recursive: true });
|
|
277
|
-
wf(filePath, decompressed.slice(offset, offset + size));
|
|
278
|
-
}
|
|
279
|
-
offset += Math.ceil(size / 512) * 512;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
function registerSyncCommands(program) {
|
|
283
|
-
const sync = program.command("sync").description("Sync skills from Headways to local Claude Code");
|
|
284
|
-
sync.command("start").description("Register device and pull latest skill catalog from Headways").option("--daemon", "Run as background daemon (60s poll loop)").action(async (opts) => {
|
|
285
|
-
const cfg = readConfig();
|
|
286
|
-
if (!cfg.token || !cfg.orgId) {
|
|
287
|
-
console.error("Not logged in. Run: headways login");
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
const apiUrl = getApiUrl();
|
|
291
|
-
let state = readSyncState();
|
|
292
|
-
if (!state.device_id || !state.device_token) {
|
|
293
|
-
console.log("Registering device with Headways\u2026");
|
|
294
|
-
try {
|
|
295
|
-
const deviceState = await registerDevice(cfg.token, cfg.orgId, apiUrl);
|
|
296
|
-
state = { ...state, ...deviceState };
|
|
297
|
-
writeSyncState(state);
|
|
298
|
-
console.log(`Device registered: ${state.device_id}`);
|
|
299
|
-
} catch (err) {
|
|
300
|
-
console.error(`Registration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const doPoll = async () => {
|
|
305
|
-
try {
|
|
306
|
-
ensureClaudeHooks();
|
|
307
|
-
const delta = await pollCatalog(state, apiUrl);
|
|
308
|
-
if (!delta) {
|
|
309
|
-
console.log("Catalog up to date.");
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
state.etag = delta.etag;
|
|
313
|
-
state.last_poll = (/* @__PURE__ */ new Date()).toISOString();
|
|
314
|
-
writeSyncState(state);
|
|
315
|
-
const pendingMap = new Map(readPending().map((p) => [p.slug, p]));
|
|
316
|
-
for (const ev of delta.events) {
|
|
317
|
-
if (ev.kind === "version_published") {
|
|
318
|
-
if (ev.channel === "auto") {
|
|
319
|
-
console.log(`Auto-installing: ${ev.skill_slug}@${ev.version}`);
|
|
320
|
-
await downloadAndMaterialize(ev.skill_slug, ev.version, state, apiUrl);
|
|
321
|
-
pendingMap.delete(ev.skill_slug);
|
|
322
|
-
} else {
|
|
323
|
-
pendingMap.set(ev.skill_slug, {
|
|
324
|
-
slug: ev.skill_slug,
|
|
325
|
-
version: ev.version,
|
|
326
|
-
user_visible_change: ev.user_visible_change,
|
|
327
|
-
channel: ev.channel,
|
|
328
|
-
capabilities_delta_empty: ev.capabilities_delta_empty
|
|
329
|
-
});
|
|
330
|
-
console.log(
|
|
331
|
-
`Queued: ${ev.skill_slug}@${ev.version}${ev.user_visible_change ? ` \u2014 ${ev.user_visible_change}` : ""}`
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
} else if (ev.kind === "skill_archived" || ev.kind === "entitlement_revoked") {
|
|
335
|
-
pendingMap.delete(ev.skill_slug);
|
|
336
|
-
console.log(`Removed: ${ev.skill_slug}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
writePending([...pendingMap.values()]);
|
|
340
|
-
console.log(`Synced. ETag: ${delta.etag}`);
|
|
341
|
-
} catch (err) {
|
|
342
|
-
console.error(`Poll error: ${err instanceof Error ? err.message : String(err)}`);
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
await doPoll();
|
|
346
|
-
if (opts.daemon) {
|
|
347
|
-
console.log("Running sync daemon (60s interval). Press Ctrl-C to stop.");
|
|
348
|
-
setInterval(doPoll, 6e4);
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
sync.command("status").description("Show current sync status and pending updates").action(() => {
|
|
352
|
-
const state = readSyncState();
|
|
353
|
-
const pending = readPending();
|
|
354
|
-
if (!state.device_id) {
|
|
355
|
-
console.log("Device not registered. Run: headways sync start");
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
console.log(`Device ID : ${state.device_id}`);
|
|
359
|
-
console.log(`Last poll : ${state.last_poll ?? "never"}`);
|
|
360
|
-
console.log(`Catalog ETag: ${state.etag ?? "none"}`);
|
|
361
|
-
if (pending.length === 0) {
|
|
362
|
-
console.log("\nAll skills up to date. No pending updates.");
|
|
363
|
-
} else {
|
|
364
|
-
console.log(`
|
|
365
|
-
Pending updates (${pending.length}):`);
|
|
366
|
-
for (const p of pending) {
|
|
367
|
-
const change = p.user_visible_change ? ` \u2014 ${p.user_visible_change}` : "";
|
|
368
|
-
const caps = p.capabilities_delta_empty ? "" : " [CAPS CHANGED]";
|
|
369
|
-
console.log(` ${p.slug}@${p.version}${change}${caps}`);
|
|
370
|
-
}
|
|
371
|
-
console.log("\nRun `headways skills accept <skill>` to install.");
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
async function acceptSkill(skillSlug) {
|
|
376
|
-
const cfg = readConfig();
|
|
377
|
-
if (!cfg.token || !cfg.orgId) {
|
|
378
|
-
console.error("Not logged in. Run: headways login");
|
|
379
|
-
process.exit(1);
|
|
380
|
-
}
|
|
381
|
-
const pending = readPending();
|
|
382
|
-
const update = pending.find((p) => p.slug === skillSlug);
|
|
383
|
-
if (!update) {
|
|
384
|
-
console.error(`No pending update for skill: ${skillSlug}`);
|
|
385
|
-
console.log("Run `headways sync status` to see pending updates.");
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
const state = readSyncState();
|
|
389
|
-
if (!state.device_id || !state.device_token) {
|
|
390
|
-
console.error("Device not registered. Run: headways sync start");
|
|
391
|
-
process.exit(1);
|
|
392
|
-
}
|
|
393
|
-
console.log(`Accepting ${skillSlug}@${update.version}\u2026`);
|
|
394
|
-
await downloadAndMaterialize(skillSlug, update.version, state, getApiUrl());
|
|
395
|
-
writePending(pending.filter((p) => p.slug !== skillSlug));
|
|
396
|
-
console.log(
|
|
397
|
-
`${skillSlug} is ready \u2014 invoke it in Claude Code with the skill's invocation phrase.`
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export {
|
|
402
|
-
registerUninstallCommand,
|
|
403
|
-
registerSetupCommand,
|
|
404
|
-
readSyncState,
|
|
405
|
-
writeSyncState,
|
|
406
|
-
registerDevice,
|
|
407
|
-
registerSyncCommands,
|
|
408
|
-
acceptSkill
|
|
409
|
-
};
|
package/dist/sync-Q3OQUWOD.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
acceptSkill,
|
|
4
|
-
readSyncState,
|
|
5
|
-
registerDevice,
|
|
6
|
-
registerSyncCommands,
|
|
7
|
-
writeSyncState
|
|
8
|
-
} from "./chunk-XTEQBKIN.js";
|
|
9
|
-
import "./chunk-UUFIIGTZ.js";
|
|
10
|
-
export {
|
|
11
|
-
acceptSkill,
|
|
12
|
-
readSyncState,
|
|
13
|
-
registerDevice,
|
|
14
|
-
registerSyncCommands,
|
|
15
|
-
writeSyncState
|
|
16
|
-
};
|