@headways/cli 0.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/dist/api-WUIL5TMR.js +10 -0
- package/dist/chunk-QXOLSB3Q.js +32 -0
- package/dist/chunk-VLKLEV4U.js +45 -0
- package/dist/config-GRE3MIQL.js +21 -0
- package/dist/index.js +783 -0
- package/package.json +39 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getApiUrl,
|
|
4
|
+
requireAuth
|
|
5
|
+
} from "./chunk-VLKLEV4U.js";
|
|
6
|
+
|
|
7
|
+
// src/lib/api.ts
|
|
8
|
+
async function rawRequest(path, token, options = {}, apiUrl) {
|
|
9
|
+
const url = `${apiUrl ?? getApiUrl()}${path}`;
|
|
10
|
+
const res = await fetch(url, {
|
|
11
|
+
...options,
|
|
12
|
+
headers: {
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
Authorization: `Bearer ${token}`,
|
|
15
|
+
...options.headers
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const body = await res.text().catch(() => "");
|
|
20
|
+
throw new Error(`API ${res.status}: ${body}`);
|
|
21
|
+
}
|
|
22
|
+
return res.json();
|
|
23
|
+
}
|
|
24
|
+
async function apiRequest(path, options = {}) {
|
|
25
|
+
const { token } = requireAuth();
|
|
26
|
+
return rawRequest(path, token, options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
rawRequest,
|
|
31
|
+
apiRequest
|
|
32
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/config.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
var HEADWAYS_DIR = join(homedir(), ".headways");
|
|
8
|
+
var CONFIG_FILE = join(HEADWAYS_DIR, "config.json");
|
|
9
|
+
var CATALOG_FILE = join(HEADWAYS_DIR, "catalog.json");
|
|
10
|
+
var INSTALLED_DIR = join(HEADWAYS_DIR, "installed");
|
|
11
|
+
function readConfig() {
|
|
12
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeConfig(cfg) {
|
|
20
|
+
if (!existsSync(HEADWAYS_DIR)) mkdirSync(HEADWAYS_DIR, { recursive: true });
|
|
21
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
|
|
22
|
+
}
|
|
23
|
+
function getApiUrl() {
|
|
24
|
+
const cfg = readConfig();
|
|
25
|
+
return cfg.apiUrl ?? process.env["HEADWAYS_API_URL"] ?? "https://api.headways.ai";
|
|
26
|
+
}
|
|
27
|
+
function requireAuth() {
|
|
28
|
+
const cfg = readConfig();
|
|
29
|
+
if (!cfg.token || !cfg.orgId) {
|
|
30
|
+
console.error("No API key configured. Run: headways configure");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
return { token: cfg.token, orgId: cfg.orgId };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
HEADWAYS_DIR,
|
|
38
|
+
CONFIG_FILE,
|
|
39
|
+
CATALOG_FILE,
|
|
40
|
+
INSTALLED_DIR,
|
|
41
|
+
readConfig,
|
|
42
|
+
writeConfig,
|
|
43
|
+
getApiUrl,
|
|
44
|
+
requireAuth
|
|
45
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
CATALOG_FILE,
|
|
4
|
+
CONFIG_FILE,
|
|
5
|
+
HEADWAYS_DIR,
|
|
6
|
+
INSTALLED_DIR,
|
|
7
|
+
getApiUrl,
|
|
8
|
+
readConfig,
|
|
9
|
+
requireAuth,
|
|
10
|
+
writeConfig
|
|
11
|
+
} from "./chunk-VLKLEV4U.js";
|
|
12
|
+
export {
|
|
13
|
+
CATALOG_FILE,
|
|
14
|
+
CONFIG_FILE,
|
|
15
|
+
HEADWAYS_DIR,
|
|
16
|
+
INSTALLED_DIR,
|
|
17
|
+
getApiUrl,
|
|
18
|
+
readConfig,
|
|
19
|
+
requireAuth,
|
|
20
|
+
writeConfig
|
|
21
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
apiRequest,
|
|
4
|
+
rawRequest
|
|
5
|
+
} from "./chunk-QXOLSB3Q.js";
|
|
6
|
+
import {
|
|
7
|
+
HEADWAYS_DIR,
|
|
8
|
+
getApiUrl,
|
|
9
|
+
readConfig,
|
|
10
|
+
requireAuth,
|
|
11
|
+
writeConfig
|
|
12
|
+
} from "./chunk-VLKLEV4U.js";
|
|
13
|
+
|
|
14
|
+
// src/index.ts
|
|
15
|
+
import "dotenv/config";
|
|
16
|
+
import { program } from "commander";
|
|
17
|
+
|
|
18
|
+
// src/commands/auth.ts
|
|
19
|
+
import "commander";
|
|
20
|
+
import * as readline from "readline/promises";
|
|
21
|
+
async function promptApiKey(opts) {
|
|
22
|
+
let token = opts.token;
|
|
23
|
+
if (!token) {
|
|
24
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
25
|
+
token = (await rl.question("API key (sk_\u2026): ")).trim();
|
|
26
|
+
rl.close();
|
|
27
|
+
}
|
|
28
|
+
if (!token) {
|
|
29
|
+
console.error("No API key provided.");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
return token;
|
|
33
|
+
}
|
|
34
|
+
function registerAuthCommands(program2) {
|
|
35
|
+
const configure = program2.command("configure").description("Set your Headways API key and optional API URL").option("--token <token>", "API key (skip interactive prompt)").option("--api-url <url>", "API base URL (default: https://api.headways.ai)").option("--app-url <url>", "Web app URL (default: https://app.headways.ai)").action(async (opts) => {
|
|
36
|
+
const token = await promptApiKey(opts);
|
|
37
|
+
const cfg = readConfig();
|
|
38
|
+
if (opts.apiUrl) cfg.apiUrl = opts.apiUrl;
|
|
39
|
+
if (opts.appUrl) cfg.appUrl = opts.appUrl;
|
|
40
|
+
try {
|
|
41
|
+
const me = await rawRequest(
|
|
42
|
+
"/v1/me",
|
|
43
|
+
token,
|
|
44
|
+
{},
|
|
45
|
+
cfg.apiUrl
|
|
46
|
+
);
|
|
47
|
+
cfg.token = token;
|
|
48
|
+
if (me.type === "api_key" && me.organization) {
|
|
49
|
+
cfg.orgSlug = me.organization.slug;
|
|
50
|
+
cfg.orgId = me.organization.id;
|
|
51
|
+
}
|
|
52
|
+
writeConfig(cfg);
|
|
53
|
+
console.log(me.type === "api_key" && me.organization ? `API key saved. Org: ${me.organization.slug}` : "API key saved.");
|
|
54
|
+
} catch {
|
|
55
|
+
console.error("Invalid API key. Not saved. Check the key and try again.");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
configure.command("status").description("Show current API key and org").action(() => {
|
|
60
|
+
const cfg = readConfig();
|
|
61
|
+
if (!cfg.token) {
|
|
62
|
+
console.log("No API key configured. Run `headways configure`.");
|
|
63
|
+
} else {
|
|
64
|
+
console.log(`API key: ${cfg.token.slice(0, 12)}\u2026 | Org: ${cfg.orgSlug ?? "(none set)"} | API: ${cfg.apiUrl ?? "https://api.headways.ai"}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
configure.command("clear").description("Remove stored API key and org").action(() => {
|
|
68
|
+
const cfg = readConfig();
|
|
69
|
+
delete cfg.token;
|
|
70
|
+
delete cfg.orgSlug;
|
|
71
|
+
delete cfg.orgId;
|
|
72
|
+
writeConfig(cfg);
|
|
73
|
+
console.log("Configuration cleared.");
|
|
74
|
+
});
|
|
75
|
+
program2.command("org").description("Manage org context").command("use <slug>").description("Set active org by slug").action(async (slug) => {
|
|
76
|
+
const cfg = readConfig();
|
|
77
|
+
if (!cfg.token) {
|
|
78
|
+
console.error("No API key configured. Run `headways configure` first.");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
await rawRequest("/v1/me", cfg.token);
|
|
83
|
+
} catch {
|
|
84
|
+
console.error("Invalid API key. Run `headways configure` again.");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
cfg.orgSlug = slug;
|
|
88
|
+
writeConfig(cfg);
|
|
89
|
+
console.log(`Active org set to: ${slug}`);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/commands/skills/index.ts
|
|
94
|
+
import "commander";
|
|
95
|
+
|
|
96
|
+
// src/commands/skills/new.ts
|
|
97
|
+
import "commander";
|
|
98
|
+
import * as fs from "fs/promises";
|
|
99
|
+
import * as path from "path";
|
|
100
|
+
import * as readline2 from "readline/promises";
|
|
101
|
+
function registerNewCommand(program2) {
|
|
102
|
+
program2.command("new").description("Create a new skill scaffold in the current directory").option("--slug <slug>", "Skill slug").option("--headline <headline>", "Verb-first headline").action(async (opts) => {
|
|
103
|
+
requireAuth();
|
|
104
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
105
|
+
const slug = opts.slug ?? (await rl.question("Slug (e.g. email-triage): ")).trim();
|
|
106
|
+
const headline = opts.headline ?? (await rl.question("Headline (verb-first, \u226490 chars): ")).trim();
|
|
107
|
+
rl.close();
|
|
108
|
+
if (!slug || !headline) {
|
|
109
|
+
console.error("slug and headline are required.");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
const dir = path.join(process.cwd(), slug);
|
|
113
|
+
await fs.mkdir(dir, { recursive: true });
|
|
114
|
+
await fs.writeFile(
|
|
115
|
+
path.join(dir, "SKILL.md"),
|
|
116
|
+
`---
|
|
117
|
+
description: ${headline}
|
|
118
|
+
allowed_tools:
|
|
119
|
+
- Read
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
# ${slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
|
|
123
|
+
|
|
124
|
+
[Add your skill instructions here]
|
|
125
|
+
`
|
|
126
|
+
);
|
|
127
|
+
await fs.writeFile(
|
|
128
|
+
path.join(dir, "headways.yaml"),
|
|
129
|
+
`slug: ${slug}
|
|
130
|
+
name: ${slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}
|
|
131
|
+
headline: '${headline}'
|
|
132
|
+
channel: prompt
|
|
133
|
+
runtimes: [claude-code]
|
|
134
|
+
`
|
|
135
|
+
);
|
|
136
|
+
await fs.writeFile(
|
|
137
|
+
path.join(dir, "capabilities.yaml"),
|
|
138
|
+
`capabilities:
|
|
139
|
+
reads: []
|
|
140
|
+
writes: []
|
|
141
|
+
external: []
|
|
142
|
+
data_classes: []
|
|
143
|
+
auto_send: false
|
|
144
|
+
`
|
|
145
|
+
);
|
|
146
|
+
await fs.writeFile(
|
|
147
|
+
path.join(dir, "hooks.yaml"),
|
|
148
|
+
`hooks:
|
|
149
|
+
- name: outcome
|
|
150
|
+
kind: outcome
|
|
151
|
+
description: 'Captures the main output artifact'
|
|
152
|
+
schema: {}
|
|
153
|
+
`
|
|
154
|
+
);
|
|
155
|
+
await fs.mkdir(path.join(dir, "fixtures"), { recursive: true });
|
|
156
|
+
try {
|
|
157
|
+
await apiRequest("/v1/skills", {
|
|
158
|
+
method: "POST",
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
slug,
|
|
161
|
+
name: slug.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
162
|
+
headline
|
|
163
|
+
})
|
|
164
|
+
});
|
|
165
|
+
console.log(`\u2713 Skill '${slug}' scaffolded and registered at ${dir}`);
|
|
166
|
+
} catch {
|
|
167
|
+
console.log(
|
|
168
|
+
`\u2713 Skill '${slug}' scaffolded at ${dir} (not yet synced \u2014 run 'headways skills push' to register)`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/commands/skills/import.ts
|
|
175
|
+
import "commander";
|
|
176
|
+
import * as fs2 from "fs/promises";
|
|
177
|
+
import * as path2 from "path";
|
|
178
|
+
var ORG_PROFILES = {
|
|
179
|
+
hippocratic: {
|
|
180
|
+
connectorHints: ["ehr.read", "ehr.write", "phi.access"],
|
|
181
|
+
fixtureTemplate: "patient-encounter",
|
|
182
|
+
channelPolicy: "stable"
|
|
183
|
+
},
|
|
184
|
+
revive: {
|
|
185
|
+
connectorHints: ["email.read", "calendar.read", "ads.read"],
|
|
186
|
+
fixtureTemplate: "campaign-brief",
|
|
187
|
+
channelPolicy: "beta"
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
function registerImportCommand(program2) {
|
|
191
|
+
program2.command("import <path>").description("Import a skill from a file, directory, or URL").option("--slug <slug>", "Override derived slug").option(
|
|
192
|
+
"--org <orgProfile>",
|
|
193
|
+
"Apply org-specific connector hints + fixture templates (hippocratic, revive)"
|
|
194
|
+
).action(async (inputPath, opts) => {
|
|
195
|
+
requireAuth();
|
|
196
|
+
let source;
|
|
197
|
+
let format = "auto";
|
|
198
|
+
try {
|
|
199
|
+
const stat2 = await fs2.stat(inputPath);
|
|
200
|
+
if (stat2.isDirectory()) {
|
|
201
|
+
const skillMdPath = path2.join(inputPath, "SKILL.md");
|
|
202
|
+
source = await fs2.readFile(skillMdPath, "utf-8");
|
|
203
|
+
format = "skill-md";
|
|
204
|
+
} else {
|
|
205
|
+
source = await fs2.readFile(inputPath, "utf-8");
|
|
206
|
+
if (inputPath.endsWith(".yaml") || inputPath.endsWith(".yml")) {
|
|
207
|
+
format = "headways-yaml";
|
|
208
|
+
} else {
|
|
209
|
+
format = "markdown";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
console.error(`Cannot read '${inputPath}': file or directory not found.`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
const profile = opts.org ? ORG_PROFILES[opts.org] : void 0;
|
|
217
|
+
if (opts.org && !profile) {
|
|
218
|
+
console.warn(
|
|
219
|
+
`Unknown org profile '${opts.org}'. Known profiles: ${Object.keys(ORG_PROFILES).join(", ")}`
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const derivedSlug = opts.slug ?? path2.basename(inputPath, path2.extname(inputPath)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
223
|
+
const result = await apiRequest("/v1/skills/import", {
|
|
224
|
+
method: "POST",
|
|
225
|
+
body: JSON.stringify({
|
|
226
|
+
source,
|
|
227
|
+
format,
|
|
228
|
+
suggestedSlug: derivedSlug,
|
|
229
|
+
connectorHints: profile?.connectorHints,
|
|
230
|
+
channelPolicy: profile?.channelPolicy
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
console.log(`Imported as '${result.slug}' (format: ${result.detectedFormat})`);
|
|
234
|
+
console.log(` Headline: ${result.headline}`);
|
|
235
|
+
console.log(` Skill ID: ${result.skillId}`);
|
|
236
|
+
if (profile) {
|
|
237
|
+
console.log(` Org profile: ${opts.org}`);
|
|
238
|
+
console.log(` Connector hints: ${profile.connectorHints.join(", ")}`);
|
|
239
|
+
console.log(` Fixture template: ${profile.fixtureTemplate}`);
|
|
240
|
+
}
|
|
241
|
+
console.log(` Run 'headways skills push ${result.slug}' to sync edits.`);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/commands/skills/push.ts
|
|
246
|
+
import "commander";
|
|
247
|
+
import * as fs3 from "fs/promises";
|
|
248
|
+
import * as path3 from "path";
|
|
249
|
+
import { watch } from "fs";
|
|
250
|
+
async function readSkillDir(dir) {
|
|
251
|
+
const skillMdPath = path3.join(dir, "SKILL.md");
|
|
252
|
+
const body = await fs3.readFile(skillMdPath, "utf-8").catch(() => "");
|
|
253
|
+
let headline;
|
|
254
|
+
const headwaysYamlPath = path3.join(dir, "headways.yaml");
|
|
255
|
+
try {
|
|
256
|
+
const yaml = await fs3.readFile(headwaysYamlPath, "utf-8");
|
|
257
|
+
const match = yaml.match(/headline:\s*['"]?(.+?)['"]?\s*$/m);
|
|
258
|
+
if (match) headline = match[1] ?? void 0;
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
let capabilities;
|
|
262
|
+
const capYamlPath = path3.join(dir, "capabilities.yaml");
|
|
263
|
+
try {
|
|
264
|
+
const capYaml = await fs3.readFile(capYamlPath, "utf-8");
|
|
265
|
+
capabilities = { raw: capYaml };
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
return { body, headline, capabilities };
|
|
269
|
+
}
|
|
270
|
+
async function pushSkill(slug, dir) {
|
|
271
|
+
const { body, headline, capabilities } = await readSkillDir(dir);
|
|
272
|
+
await apiRequest(`/v1/skills/${slug}/draft`, {
|
|
273
|
+
method: "PUT",
|
|
274
|
+
body: JSON.stringify({
|
|
275
|
+
body,
|
|
276
|
+
...headline ? { headline } : {},
|
|
277
|
+
...capabilities ? { capabilities } : {}
|
|
278
|
+
})
|
|
279
|
+
});
|
|
280
|
+
console.log(`Pushed '${slug}' draft`);
|
|
281
|
+
}
|
|
282
|
+
function registerPushCommand(program2) {
|
|
283
|
+
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: ./<slug> or cwd)").action(async (slug, opts) => {
|
|
284
|
+
requireAuth();
|
|
285
|
+
const resolvedSlug = slug ?? path3.basename(process.cwd());
|
|
286
|
+
const dir = opts.dir ?? (slug ? path3.join(process.cwd(), slug) : process.cwd());
|
|
287
|
+
await pushSkill(resolvedSlug, dir);
|
|
288
|
+
if (opts.watch) {
|
|
289
|
+
console.log(`Watching ${dir} for changes...`);
|
|
290
|
+
let debounceTimer = null;
|
|
291
|
+
watch(dir, { recursive: true }, () => {
|
|
292
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
293
|
+
debounceTimer = setTimeout(async () => {
|
|
294
|
+
try {
|
|
295
|
+
await pushSkill(resolvedSlug, dir);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error("Push failed:", err.message);
|
|
298
|
+
}
|
|
299
|
+
}, 300);
|
|
300
|
+
});
|
|
301
|
+
await new Promise(() => {
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/commands/skills/capture.ts
|
|
308
|
+
import "commander";
|
|
309
|
+
import * as fs4 from "fs/promises";
|
|
310
|
+
import * as path4 from "path";
|
|
311
|
+
import { homedir } from "os";
|
|
312
|
+
var SETTINGS_PATH = path4.join(homedir(), ".claude", "settings.json");
|
|
313
|
+
async function readSettingsJson() {
|
|
314
|
+
try {
|
|
315
|
+
const raw = await fs4.readFile(SETTINGS_PATH, "utf8");
|
|
316
|
+
return JSON.parse(raw);
|
|
317
|
+
} catch {
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function writeSettingsJson(settings) {
|
|
322
|
+
await fs4.mkdir(path4.dirname(SETTINGS_PATH), { recursive: true });
|
|
323
|
+
await fs4.writeFile(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
324
|
+
}
|
|
325
|
+
async function injectCaptureHooks(slug, apiUrl, token) {
|
|
326
|
+
const settings = await readSettingsJson();
|
|
327
|
+
const hooks = settings["hooks"] ?? {};
|
|
328
|
+
const postToolUse = hooks["PostToolUse"] ?? [];
|
|
329
|
+
const preToolUse = hooks["PreToolUse"] ?? [];
|
|
330
|
+
const postHookId = `headways-capture-post-${slug}`;
|
|
331
|
+
const preHookId = `headways-capture-pre-${slug}`;
|
|
332
|
+
if (!postToolUse.some((h) => h["id"] === postHookId)) {
|
|
333
|
+
postToolUse.push({
|
|
334
|
+
id: postHookId,
|
|
335
|
+
matcher: "*",
|
|
336
|
+
hooks: [
|
|
337
|
+
{
|
|
338
|
+
type: "command",
|
|
339
|
+
command: `headways capture-record --slug ${slug} --event tool.{{tool_name}} --status {{tool_result_is_error}} 2>/dev/null || true`
|
|
340
|
+
}
|
|
341
|
+
]
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
if (!preToolUse.some((h) => h["id"] === preHookId)) {
|
|
345
|
+
preToolUse.push({
|
|
346
|
+
id: preHookId,
|
|
347
|
+
matcher: "*",
|
|
348
|
+
hooks: [
|
|
349
|
+
{
|
|
350
|
+
type: "command",
|
|
351
|
+
command: `headways capture-record --slug ${slug} --event tool.pre.{{tool_name}} 2>/dev/null || true`
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
hooks["PostToolUse"] = postToolUse;
|
|
357
|
+
hooks["PreToolUse"] = preToolUse;
|
|
358
|
+
settings["hooks"] = hooks;
|
|
359
|
+
await writeSettingsJson(settings);
|
|
360
|
+
console.log(`Injected capture hooks for ${slug} into ${SETTINGS_PATH}`);
|
|
361
|
+
}
|
|
362
|
+
async function removeCaptureHooks(slug) {
|
|
363
|
+
const settings = await readSettingsJson();
|
|
364
|
+
const hooks = settings["hooks"] ?? {};
|
|
365
|
+
const postHookId = `headways-capture-post-${slug}`;
|
|
366
|
+
const preHookId = `headways-capture-pre-${slug}`;
|
|
367
|
+
if (Array.isArray(hooks["PostToolUse"])) {
|
|
368
|
+
hooks["PostToolUse"] = hooks["PostToolUse"].filter(
|
|
369
|
+
(h) => h["id"] !== postHookId
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (Array.isArray(hooks["PreToolUse"])) {
|
|
373
|
+
hooks["PreToolUse"] = hooks["PreToolUse"].filter(
|
|
374
|
+
(h) => h["id"] !== preHookId
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
settings["hooks"] = hooks;
|
|
378
|
+
await writeSettingsJson(settings);
|
|
379
|
+
console.log(`Removed capture hooks for ${slug}`);
|
|
380
|
+
}
|
|
381
|
+
function registerCaptureCommand(program2) {
|
|
382
|
+
program2.command("capture <slug>").description("Capture a live skill run session and upload as sample run").option("--version <version>", "Skill version to capture against", "draft").option("--stop", "Stop capturing and upload the session").action(async (slug, opts) => {
|
|
383
|
+
const { token, orgId } = requireAuth();
|
|
384
|
+
const apiUrl = getApiUrl();
|
|
385
|
+
if (opts.stop) {
|
|
386
|
+
await removeCaptureHooks(slug);
|
|
387
|
+
const sessionFile2 = path4.join(homedir(), ".headways", `capture-${slug}.json`);
|
|
388
|
+
let session = null;
|
|
389
|
+
try {
|
|
390
|
+
const raw = await fs4.readFile(sessionFile2, "utf8");
|
|
391
|
+
session = JSON.parse(raw);
|
|
392
|
+
} catch {
|
|
393
|
+
console.error("No active capture session found. Run `headways capture <slug>` first.");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
const res = await fetch(
|
|
397
|
+
`${apiUrl}/v1/skills/${slug}/versions/${opts.version ?? "draft"}/sample-run/capture-upload`,
|
|
398
|
+
{
|
|
399
|
+
method: "POST",
|
|
400
|
+
headers: {
|
|
401
|
+
"Content-Type": "application/json",
|
|
402
|
+
Authorization: `Bearer ${token}`,
|
|
403
|
+
"x-headways-org-id": orgId
|
|
404
|
+
},
|
|
405
|
+
body: JSON.stringify(session)
|
|
406
|
+
}
|
|
407
|
+
);
|
|
408
|
+
if (!res.ok) {
|
|
409
|
+
console.error(`Upload failed: ${res.status} ${await res.text()}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
const result = await res.json();
|
|
413
|
+
await fs4.unlink(sessionFile2).catch(() => {
|
|
414
|
+
});
|
|
415
|
+
console.log(`Capture uploaded. Run ID: ${result.skillRunId}`);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
await injectCaptureHooks(slug, apiUrl, token);
|
|
419
|
+
const sessionFile = path4.join(homedir(), ".headways", `capture-${slug}.json`);
|
|
420
|
+
await fs4.mkdir(path4.dirname(sessionFile), { recursive: true });
|
|
421
|
+
await fs4.writeFile(
|
|
422
|
+
sessionFile,
|
|
423
|
+
JSON.stringify(
|
|
424
|
+
{
|
|
425
|
+
skill_slug: slug,
|
|
426
|
+
version: opts.version ?? "draft",
|
|
427
|
+
invocation_prompt: "",
|
|
428
|
+
tool_calls: [],
|
|
429
|
+
hook_emissions: [],
|
|
430
|
+
fixture_reads: [],
|
|
431
|
+
artifact_writes: [],
|
|
432
|
+
duration_ms: 0,
|
|
433
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
434
|
+
},
|
|
435
|
+
null,
|
|
436
|
+
2
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
console.log(`Capturing ${slug}. Run the skill in Claude Code, then stop with:`);
|
|
440
|
+
console.log(` headways capture ${slug} --stop`);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/commands/skills/index.ts
|
|
445
|
+
function registerSkillsCommands(program2) {
|
|
446
|
+
const skills = program2.command("skills").description("Manage skills");
|
|
447
|
+
registerNewCommand(skills);
|
|
448
|
+
registerImportCommand(skills);
|
|
449
|
+
registerPushCommand(skills);
|
|
450
|
+
registerCaptureCommand(program2);
|
|
451
|
+
skills.command("list").description("List skills in the active org").action(async () => {
|
|
452
|
+
const { requireAuth: requireAuth2 } = await import("./config-GRE3MIQL.js");
|
|
453
|
+
const { apiRequest: apiRequest2 } = await import("./api-WUIL5TMR.js");
|
|
454
|
+
requireAuth2();
|
|
455
|
+
const result = await apiRequest2("/v1/skills");
|
|
456
|
+
if (result.data.length === 0) {
|
|
457
|
+
console.log("No skills found.");
|
|
458
|
+
} else {
|
|
459
|
+
for (const s of result.data) {
|
|
460
|
+
console.log(` ${s.slug.padEnd(30)} ${s.headline}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/commands/sync/index.ts
|
|
467
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync } from "fs";
|
|
468
|
+
import { homedir as homedir2 } from "os";
|
|
469
|
+
import { join as join5 } from "path";
|
|
470
|
+
import { createGunzip } from "zlib";
|
|
471
|
+
import { Readable } from "stream";
|
|
472
|
+
import "stream/promises";
|
|
473
|
+
import "commander";
|
|
474
|
+
var PENDING_FILE = join5(HEADWAYS_DIR, "pending.json");
|
|
475
|
+
var SYNC_STATE_FILE = join5(HEADWAYS_DIR, "sync-state.json");
|
|
476
|
+
var INSTALLED_DIR = join5(HEADWAYS_DIR, "installed");
|
|
477
|
+
function readSyncState() {
|
|
478
|
+
if (!existsSync(SYNC_STATE_FILE)) return {};
|
|
479
|
+
try {
|
|
480
|
+
return JSON.parse(readFileSync(SYNC_STATE_FILE, "utf8"));
|
|
481
|
+
} catch {
|
|
482
|
+
return {};
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
function writeSyncState(state) {
|
|
486
|
+
if (!existsSync(HEADWAYS_DIR)) mkdirSync(HEADWAYS_DIR, { recursive: true });
|
|
487
|
+
writeFileSync(SYNC_STATE_FILE, JSON.stringify(state, null, 2) + "\n");
|
|
488
|
+
}
|
|
489
|
+
function readPending() {
|
|
490
|
+
if (!existsSync(PENDING_FILE)) return [];
|
|
491
|
+
try {
|
|
492
|
+
return JSON.parse(readFileSync(PENDING_FILE, "utf8"));
|
|
493
|
+
} catch {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function writePending(updates) {
|
|
498
|
+
if (!existsSync(HEADWAYS_DIR)) mkdirSync(HEADWAYS_DIR, { recursive: true });
|
|
499
|
+
writeFileSync(PENDING_FILE, JSON.stringify(updates, null, 2) + "\n");
|
|
500
|
+
}
|
|
501
|
+
function deviceHeaders(state) {
|
|
502
|
+
return {
|
|
503
|
+
Authorization: `Bearer ${state.device_token ?? ""}`,
|
|
504
|
+
"x-headways-device-id": state.device_id ?? "",
|
|
505
|
+
"x-headways-timestamp": String(Math.floor(Date.now() / 1e3))
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
async function registerDevice(token, orgId, apiUrl) {
|
|
509
|
+
const res = await fetch(`${apiUrl}/v1/sync/devices/register`, {
|
|
510
|
+
method: "POST",
|
|
511
|
+
headers: {
|
|
512
|
+
"Content-Type": "application/json",
|
|
513
|
+
Authorization: `Bearer ${token}`,
|
|
514
|
+
"x-headways-org-id": orgId
|
|
515
|
+
},
|
|
516
|
+
body: JSON.stringify({
|
|
517
|
+
publicKey: Buffer.from(`stub-pubkey-${Date.now()}`).toString("base64url"),
|
|
518
|
+
platform: process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux",
|
|
519
|
+
hostname: (await import("os")).hostname()
|
|
520
|
+
})
|
|
521
|
+
});
|
|
522
|
+
if (!res.ok) throw new Error(`Device registration failed: ${res.status}`);
|
|
523
|
+
const data = await res.json();
|
|
524
|
+
return { device_id: data.deviceId, device_token: data.deviceToken };
|
|
525
|
+
}
|
|
526
|
+
async function pollCatalog(state, apiUrl) {
|
|
527
|
+
const url = state.etag ? `${apiUrl}/v1/sync/catalog?since=${encodeURIComponent(state.etag)}` : `${apiUrl}/v1/sync/catalog`;
|
|
528
|
+
const res = await fetch(url, { headers: deviceHeaders(state) });
|
|
529
|
+
if (res.status === 304) return null;
|
|
530
|
+
if (!res.ok) throw new Error(`Catalog poll failed: ${res.status}`);
|
|
531
|
+
return res.json();
|
|
532
|
+
}
|
|
533
|
+
async function downloadAndMaterialize(slug, version, state, apiUrl) {
|
|
534
|
+
const res = await fetch(`${apiUrl}/v1/sync/bundles/${slug}/${version}`, {
|
|
535
|
+
redirect: "follow",
|
|
536
|
+
headers: deviceHeaders(state)
|
|
537
|
+
});
|
|
538
|
+
if (!res.ok) throw new Error(`Bundle fetch failed: ${res.status}`);
|
|
539
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
540
|
+
const skillsDir = join5(homedir2(), ".claude", "skills");
|
|
541
|
+
const dest = join5(skillsDir, slug);
|
|
542
|
+
const staging = join5(skillsDir, `.${slug}-staging`);
|
|
543
|
+
mkdirSync(staging, { recursive: true });
|
|
544
|
+
await extractTarGz(buf, staging);
|
|
545
|
+
if (existsSync(dest)) rmSync(dest, { recursive: true });
|
|
546
|
+
renameSync(staging, dest);
|
|
547
|
+
mkdirSync(INSTALLED_DIR, { recursive: true });
|
|
548
|
+
writeFileSync(
|
|
549
|
+
join5(INSTALLED_DIR, `${slug}.json`),
|
|
550
|
+
JSON.stringify(
|
|
551
|
+
{ slug, version, runtime: "claude-code", installed_at: (/* @__PURE__ */ new Date()).toISOString() },
|
|
552
|
+
null,
|
|
553
|
+
2
|
|
554
|
+
)
|
|
555
|
+
);
|
|
556
|
+
console.log(`Materialized ${slug}@${version} \u2192 ${dest}`);
|
|
557
|
+
}
|
|
558
|
+
async function extractTarGz(buf, destDir) {
|
|
559
|
+
const decompressed = await new Promise((resolve, reject) => {
|
|
560
|
+
const chunks = [];
|
|
561
|
+
const gunzip = createGunzip();
|
|
562
|
+
const src = Readable.from(buf);
|
|
563
|
+
src.pipe(gunzip);
|
|
564
|
+
gunzip.on("data", (chunk) => chunks.push(chunk));
|
|
565
|
+
gunzip.on("end", () => resolve(Buffer.concat(chunks)));
|
|
566
|
+
gunzip.on("error", reject);
|
|
567
|
+
});
|
|
568
|
+
let offset = 0;
|
|
569
|
+
const { writeFileSync: wf, mkdirSync: md } = await import("fs");
|
|
570
|
+
const { dirname: dirname2 } = await import("path");
|
|
571
|
+
while (offset + 512 <= decompressed.length) {
|
|
572
|
+
const header = decompressed.slice(offset, offset + 512);
|
|
573
|
+
const name = header.slice(0, 100).toString("utf8").replace(/\0/g, "").trim();
|
|
574
|
+
if (!name) break;
|
|
575
|
+
const sizeOctal = header.slice(124, 136).toString("utf8").replace(/\0/g, "").trim();
|
|
576
|
+
const size = parseInt(sizeOctal, 8) || 0;
|
|
577
|
+
const typeFlag = header[156];
|
|
578
|
+
offset += 512;
|
|
579
|
+
if (typeFlag === 53 || name.endsWith("/")) {
|
|
580
|
+
md(join5(destDir, name), { recursive: true });
|
|
581
|
+
} else if (typeFlag === 0 || typeFlag === 48 || typeFlag === void 0) {
|
|
582
|
+
const filePath = join5(destDir, name);
|
|
583
|
+
md(dirname2(filePath), { recursive: true });
|
|
584
|
+
wf(filePath, decompressed.slice(offset, offset + size));
|
|
585
|
+
}
|
|
586
|
+
offset += Math.ceil(size / 512) * 512;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function registerSyncCommands(program2) {
|
|
590
|
+
const sync = program2.command("sync").description("Sync skills from Headways to local Claude Code");
|
|
591
|
+
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) => {
|
|
592
|
+
const cfg = readConfig();
|
|
593
|
+
if (!cfg.token || !cfg.orgId) {
|
|
594
|
+
console.error("Not logged in. Run: headways login");
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
const apiUrl = getApiUrl();
|
|
598
|
+
let state = readSyncState();
|
|
599
|
+
if (!state.device_id || !state.device_token) {
|
|
600
|
+
console.log("Registering device with Headways\u2026");
|
|
601
|
+
try {
|
|
602
|
+
const deviceState = await registerDevice(cfg.token, cfg.orgId, apiUrl);
|
|
603
|
+
state = { ...state, ...deviceState };
|
|
604
|
+
writeSyncState(state);
|
|
605
|
+
console.log(`Device registered: ${state.device_id}`);
|
|
606
|
+
} catch (err) {
|
|
607
|
+
console.error(`Registration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const doPoll = async () => {
|
|
612
|
+
try {
|
|
613
|
+
const delta = await pollCatalog(state, apiUrl);
|
|
614
|
+
if (!delta) {
|
|
615
|
+
console.log("Catalog up to date.");
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
state.etag = delta.etag;
|
|
619
|
+
state.last_poll = (/* @__PURE__ */ new Date()).toISOString();
|
|
620
|
+
writeSyncState(state);
|
|
621
|
+
const pendingMap = new Map(readPending().map((p) => [p.slug, p]));
|
|
622
|
+
for (const ev of delta.events) {
|
|
623
|
+
if (ev.kind === "version_published") {
|
|
624
|
+
if (ev.channel === "auto") {
|
|
625
|
+
console.log(`Auto-installing: ${ev.skill_slug}@${ev.version}`);
|
|
626
|
+
await downloadAndMaterialize(ev.skill_slug, ev.version, state, apiUrl);
|
|
627
|
+
pendingMap.delete(ev.skill_slug);
|
|
628
|
+
} else {
|
|
629
|
+
pendingMap.set(ev.skill_slug, {
|
|
630
|
+
slug: ev.skill_slug,
|
|
631
|
+
version: ev.version,
|
|
632
|
+
user_visible_change: ev.user_visible_change,
|
|
633
|
+
channel: ev.channel,
|
|
634
|
+
capabilities_delta_empty: ev.capabilities_delta_empty
|
|
635
|
+
});
|
|
636
|
+
console.log(
|
|
637
|
+
`Queued: ${ev.skill_slug}@${ev.version}${ev.user_visible_change ? ` \u2014 ${ev.user_visible_change}` : ""}`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
} else if (ev.kind === "skill_archived" || ev.kind === "entitlement_revoked") {
|
|
641
|
+
pendingMap.delete(ev.skill_slug);
|
|
642
|
+
console.log(`Removed: ${ev.skill_slug}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
writePending([...pendingMap.values()]);
|
|
646
|
+
console.log(`Synced. ETag: ${delta.etag}`);
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.error(`Poll error: ${err instanceof Error ? err.message : String(err)}`);
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
await doPoll();
|
|
652
|
+
if (opts.daemon) {
|
|
653
|
+
console.log("Running sync daemon (60s interval). Press Ctrl-C to stop.");
|
|
654
|
+
setInterval(doPoll, 6e4);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
sync.command("status").description("Show current sync status and pending updates").action(() => {
|
|
658
|
+
const state = readSyncState();
|
|
659
|
+
const pending = readPending();
|
|
660
|
+
if (!state.device_id) {
|
|
661
|
+
console.log("Device not registered. Run: headways sync start");
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
console.log(`Device ID : ${state.device_id}`);
|
|
665
|
+
console.log(`Last poll : ${state.last_poll ?? "never"}`);
|
|
666
|
+
console.log(`Catalog ETag: ${state.etag ?? "none"}`);
|
|
667
|
+
if (pending.length === 0) {
|
|
668
|
+
console.log("\nAll skills up to date. No pending updates.");
|
|
669
|
+
} else {
|
|
670
|
+
console.log(`
|
|
671
|
+
Pending updates (${pending.length}):`);
|
|
672
|
+
for (const p of pending) {
|
|
673
|
+
const change = p.user_visible_change ? ` \u2014 ${p.user_visible_change}` : "";
|
|
674
|
+
const caps = p.capabilities_delta_empty ? "" : " [CAPS CHANGED]";
|
|
675
|
+
console.log(` ${p.slug}@${p.version}${change}${caps}`);
|
|
676
|
+
}
|
|
677
|
+
console.log("\nRun `headways accept <skill>` to install.");
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
program2.command("accept <skill>").description("Accept a pending skill update and materialize it locally").action(async (skillSlug) => {
|
|
681
|
+
const cfg = readConfig();
|
|
682
|
+
if (!cfg.token || !cfg.orgId) {
|
|
683
|
+
console.error("Not logged in. Run: headways login");
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
const pending = readPending();
|
|
687
|
+
const update = pending.find((p) => p.slug === skillSlug);
|
|
688
|
+
if (!update) {
|
|
689
|
+
console.error(`No pending update for skill: ${skillSlug}`);
|
|
690
|
+
console.log("Run `headways sync status` to see pending updates.");
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
const state = readSyncState();
|
|
694
|
+
if (!state.device_id || !state.device_token) {
|
|
695
|
+
console.error("Device not registered. Run: headways sync start");
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
console.log(`Accepting ${skillSlug}@${update.version}\u2026`);
|
|
699
|
+
await downloadAndMaterialize(skillSlug, update.version, state, getApiUrl());
|
|
700
|
+
writePending(pending.filter((p) => p.slug !== skillSlug));
|
|
701
|
+
console.log(
|
|
702
|
+
`${skillSlug} is ready \u2014 invoke it in Claude Code with the skill's invocation phrase.`
|
|
703
|
+
);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/commands/feedback.ts
|
|
708
|
+
import "commander";
|
|
709
|
+
function registerFeedbackCommand(program2) {
|
|
710
|
+
program2.command("feedback <skillSlug>").description("Submit feedback about a skill (posts to Headways API)").option(
|
|
711
|
+
"--reaction <type>",
|
|
712
|
+
"Reaction type: thumbs_up, thumbs_down, wrong_output, missing_step",
|
|
713
|
+
"thumbs_down"
|
|
714
|
+
).option("--note <text>", "Free-text note about the issue").option("--run-id <runId>", "Skill run ID (or set HEADWAYS_RUN_ID env var)").action(
|
|
715
|
+
async (skillSlug, opts) => {
|
|
716
|
+
const cfg = readConfig();
|
|
717
|
+
if (!cfg.token) {
|
|
718
|
+
console.error("Not authenticated. Run `headways auth login` first.");
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
const runId = opts.runId ?? process.env["HEADWAYS_RUN_ID"];
|
|
722
|
+
try {
|
|
723
|
+
await rawRequest(`/v1/skills/${skillSlug}/feedback?source=runtime_cli`, cfg.token, {
|
|
724
|
+
method: "POST",
|
|
725
|
+
body: JSON.stringify({ reaction: opts.reaction, note: opts.note, runId })
|
|
726
|
+
});
|
|
727
|
+
console.log(`Feedback submitted for ${skillSlug}.`);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
console.error(`Error: ${String(err)}`);
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/sdk/emit.ts
|
|
737
|
+
import "commander";
|
|
738
|
+
function registerEmitCommand(program2) {
|
|
739
|
+
program2.command("emit").description("Emit a skill run event (used by Claude Code hooks)").option("--run-id <runId>", "Skill run ID (or set HEADWAYS_RUN_ID env var)").option("--hook <hookName>", "Hook name to emit").option("--event <event>", "Legacy alias for --hook").option("--status <status>", "Tool result status (true=error, false=ok)").option("-f, --field <entries...>", "Field values in key=value format").allowUnknownOption(true).action(
|
|
740
|
+
async (opts) => {
|
|
741
|
+
const cfg = readConfig();
|
|
742
|
+
if (!cfg.token) return;
|
|
743
|
+
const runId = opts.runId ?? process.env["HEADWAYS_RUN_ID"];
|
|
744
|
+
if (!runId) return;
|
|
745
|
+
const hookName = opts.hook ?? opts.event ?? "tool.unknown";
|
|
746
|
+
const payload = {};
|
|
747
|
+
if (opts.status !== void 0) {
|
|
748
|
+
payload["tool_result_is_error"] = opts.status === "true" || opts.status === "1";
|
|
749
|
+
}
|
|
750
|
+
for (const entry of opts.field ?? []) {
|
|
751
|
+
const eqIdx = entry.indexOf("=");
|
|
752
|
+
if (eqIdx > 0) {
|
|
753
|
+
const k = entry.slice(0, eqIdx);
|
|
754
|
+
const v = entry.slice(eqIdx + 1);
|
|
755
|
+
payload[k] = v;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const apiUrl = getApiUrl();
|
|
759
|
+
try {
|
|
760
|
+
await fetch(`${apiUrl}/v1/skill-runs/${runId}/events`, {
|
|
761
|
+
method: "POST",
|
|
762
|
+
headers: {
|
|
763
|
+
"Content-Type": "application/json",
|
|
764
|
+
Authorization: `Bearer ${cfg.token}`
|
|
765
|
+
},
|
|
766
|
+
body: JSON.stringify({
|
|
767
|
+
events: [{ hookName, payload, ts: Date.now() }]
|
|
768
|
+
})
|
|
769
|
+
});
|
|
770
|
+
} catch {
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// src/index.ts
|
|
777
|
+
program.name("headways").description("Headways CLI \u2014 skill authoring, sync, and runtime SDK").version("0.1.0");
|
|
778
|
+
registerAuthCommands(program);
|
|
779
|
+
registerSkillsCommands(program);
|
|
780
|
+
registerSyncCommands(program);
|
|
781
|
+
registerFeedbackCommand(program);
|
|
782
|
+
registerEmitCommand(program);
|
|
783
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@headways/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Headways CLI — authoring, sync, and runtime SDK",
|
|
6
|
+
"files": ["dist", "README.md"],
|
|
7
|
+
"bin": {
|
|
8
|
+
"headways": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsx src/index.ts",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:unit": "vitest run",
|
|
18
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
19
|
+
"prepublishOnly": "pnpm build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@headways/db": "workspace:*",
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"commander": "^12.1.0",
|
|
26
|
+
"dotenv": "^16.4.7",
|
|
27
|
+
"node-fetch": "^3.3.2",
|
|
28
|
+
"yaml": "^2.5.1",
|
|
29
|
+
"zod": "^3.25.28"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@headways/config": "workspace:*",
|
|
33
|
+
"@types/node": "^22.16.5",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
35
|
+
"tsx": "^4.21.0",
|
|
36
|
+
"typescript": "^5.8.3",
|
|
37
|
+
"vitest": "^3.2.4"
|
|
38
|
+
}
|
|
39
|
+
}
|