@headways/cli 0.3.0 → 0.4.1

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.
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  apiRequest,
4
4
  rawRequest
5
- } from "./chunk-HYEL7L5Z.js";
6
- import "./chunk-T2H7EXOV.js";
5
+ } from "./chunk-2INXZHRG.js";
6
+ import "./chunk-UUFIIGTZ.js";
7
7
  export {
8
8
  apiRequest,
9
9
  rawRequest
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getApiUrl,
4
4
  requireAuth
5
- } from "./chunk-T2H7EXOV.js";
5
+ } from "./chunk-UUFIIGTZ.js";
6
6
 
7
7
  // src/lib/api.ts
8
8
  async function rawRequest(path, token, options = {}, apiUrl) {
@@ -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 CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
11
12
  function readConfig() {
12
13
  if (!existsSync(CONFIG_FILE)) return {};
13
14
  try {
@@ -42,6 +43,7 @@ export {
42
43
  CONFIG_FILE,
43
44
  CATALOG_FILE,
44
45
  INSTALLED_DIR,
46
+ CLAUDE_SKILLS_DIR,
45
47
  readConfig,
46
48
  writeConfig,
47
49
  getApiUrl,
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CLAUDE_SKILLS_DIR,
3
4
  HEADWAYS_DIR,
4
5
  getApiUrl,
5
6
  readConfig
6
- } from "./chunk-T2H7EXOV.js";
7
+ } from "./chunk-UUFIIGTZ.js";
7
8
 
8
9
  // src/commands/sync/index.ts
9
10
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, renameSync, rmSync as rmSync2 } from "fs";
10
- import { homedir as homedir2 } from "os";
11
11
  import { join as join2 } from "path";
12
12
  import { createGunzip } from "zlib";
13
13
  import { Readable } from "stream";
@@ -53,9 +53,8 @@ function registerUninstallCommand(program) {
53
53
  removeHooks(CLAUDE_SETTINGS_GLOBAL);
54
54
  removeHooks(CLAUDE_SETTINGS_LOCAL);
55
55
  console.log("\u2713 Removed Claude Code hooks");
56
- const skillsDir = join(homedir(), ".claude", "skills");
57
- if (existsSync(skillsDir)) {
58
- rmSync(skillsDir, { recursive: true, force: true });
56
+ if (existsSync(CLAUDE_SKILLS_DIR)) {
57
+ rmSync(CLAUDE_SKILLS_DIR, { recursive: true, force: true });
59
58
  console.log("\u2713 Removed ~/.claude/skills/");
60
59
  }
61
60
  const headwaysDir = join(homedir(), ".headways");
@@ -232,9 +231,8 @@ async function downloadAndMaterialize(slug, version, state, apiUrl) {
232
231
  });
233
232
  if (!res.ok) throw new Error(`Bundle fetch failed: ${res.status}`);
234
233
  const buf = Buffer.from(await res.arrayBuffer());
235
- const skillsDir = join2(homedir2(), ".claude", "skills");
236
- const dest = join2(skillsDir, slug);
237
- const staging = join2(skillsDir, `.${slug}-staging`);
234
+ const dest = join2(CLAUDE_SKILLS_DIR, slug);
235
+ const staging = join2(CLAUDE_SKILLS_DIR, `.${slug}-staging`);
238
236
  mkdirSync2(staging, { recursive: true });
239
237
  await extractTarGz(buf, staging);
240
238
  if (existsSync2(dest)) rmSync2(dest, { recursive: true });
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CATALOG_FILE,
4
+ CLAUDE_SKILLS_DIR,
4
5
  CONFIG_FILE,
5
6
  HEADWAYS_DIR,
6
7
  INSTALLED_DIR,
@@ -9,9 +10,10 @@ import {
9
10
  readConfig,
10
11
  requireAuth,
11
12
  writeConfig
12
- } from "./chunk-T2H7EXOV.js";
13
+ } from "./chunk-UUFIIGTZ.js";
13
14
  export {
14
15
  CATALOG_FILE,
16
+ CLAUDE_SKILLS_DIR,
15
17
  CONFIG_FILE,
16
18
  HEADWAYS_DIR,
17
19
  INSTALLED_DIR,
package/dist/index.js CHANGED
@@ -2,23 +2,25 @@
2
2
  import {
3
3
  apiRequest,
4
4
  rawRequest
5
- } from "./chunk-HYEL7L5Z.js";
5
+ } from "./chunk-2INXZHRG.js";
6
6
  import {
7
7
  registerSetupCommand,
8
8
  registerSyncCommands,
9
9
  registerUninstallCommand
10
- } from "./chunk-OZULVVQC.js";
10
+ } from "./chunk-XTEQBKIN.js";
11
11
  import {
12
+ CLAUDE_SKILLS_DIR,
12
13
  INSTALLED_DIR,
13
14
  getApiUrl,
14
15
  getAppUrl,
15
16
  readConfig,
16
17
  requireAuth,
17
18
  writeConfig
18
- } from "./chunk-T2H7EXOV.js";
19
+ } from "./chunk-UUFIIGTZ.js";
19
20
 
20
21
  // src/index.ts
21
22
  import "dotenv/config";
23
+ import { createRequire } from "module";
22
24
  import { program } from "commander";
23
25
 
24
26
  // src/commands/auth.ts
@@ -79,10 +81,7 @@ function registerAuthCommands(program2) {
79
81
  });
80
82
  configure.command("clear").description("Clear credentials and reset setup state (keeps API and app URLs)").action(() => {
81
83
  const cfg = readConfig();
82
- const cleared = {};
83
- if (cfg.apiUrl) cleared.apiUrl = cfg.apiUrl;
84
- if (cfg.appUrl) cleared.appUrl = cfg.appUrl;
85
- writeConfig(cleared);
84
+ writeConfig({ apiUrl: cfg.apiUrl, appUrl: cfg.appUrl });
86
85
  console.log("Credentials cleared. The desktop app will show onboarding on next launch.");
87
86
  });
88
87
  configure.command("status").description("Show current API key and org").action(() => {
@@ -282,6 +281,13 @@ runtimes: [claude-code]
282
281
  kind: outcome
283
282
  description: 'Captures the main output artifact'
284
283
  schema: {}
284
+ `
285
+ );
286
+ await fs.writeFile(
287
+ path.join(dir, "connections.yaml"),
288
+ `# connections:
289
+ # - connector: slack
290
+ # purpose: "Post skill output to a Slack channel"
285
291
  `
286
292
  );
287
293
  await fs.mkdir(path.join(dir, "fixtures"), { recursive: true });
@@ -378,44 +384,82 @@ function registerImportCommand(program2) {
378
384
  import "commander";
379
385
  import * as fs3 from "fs/promises";
380
386
  import * as path3 from "path";
381
- import { watch } from "fs";
387
+ import { watch, existsSync } from "fs";
388
+ var catchMissing = (e) => {
389
+ if (e.code === "ENOENT") return null;
390
+ throw e;
391
+ };
382
392
  async function readSkillDir(dir) {
383
393
  const skillMdPath = path3.join(dir, "SKILL.md");
384
- const body = await fs3.readFile(skillMdPath, "utf-8").catch(() => "");
385
- let headline;
386
- const headwaysYamlPath = path3.join(dir, "headways.yaml");
394
+ let body;
387
395
  try {
388
- const yaml = await fs3.readFile(headwaysYamlPath, "utf-8");
389
- const match = yaml.match(/headline:\s*['"]?(.+?)['"]?\s*$/m);
390
- if (match) headline = match[1] ?? void 0;
396
+ body = await fs3.readFile(skillMdPath, "utf-8");
391
397
  } catch {
398
+ throw new Error(`SKILL.md not found in ${dir}`);
392
399
  }
393
- let capabilities;
394
- const capYamlPath = path3.join(dir, "capabilities.yaml");
395
- try {
396
- const capYaml = await fs3.readFile(capYamlPath, "utf-8");
397
- capabilities = { raw: capYaml };
398
- } catch {
400
+ const [headwaysYaml, capabilitiesYaml, connectionsYaml] = await Promise.all([
401
+ fs3.readFile(path3.join(dir, "headways.yaml"), "utf-8").catch(catchMissing),
402
+ fs3.readFile(path3.join(dir, "capabilities.yaml"), "utf-8").catch(catchMissing),
403
+ fs3.readFile(path3.join(dir, "connections.yaml"), "utf-8").catch(catchMissing)
404
+ ]);
405
+ let headline;
406
+ if (headwaysYaml) {
407
+ const match = headwaysYaml.match(/headline:\s*['"]?(.+?)['"]?\s*$/m);
408
+ if (match) headline = match[1] ?? void 0;
409
+ }
410
+ let connections;
411
+ if (connectionsYaml) {
412
+ const items = parseConnectionsYaml(connectionsYaml);
413
+ if (items.length > 0) connections = items;
414
+ }
415
+ return { body, headline, capabilities: capabilitiesYaml ?? void 0, connections };
416
+ }
417
+ function parseConnectionsYaml(yaml) {
418
+ const items = [];
419
+ const connectorRe = /^\s*-\s+connector:\s*(.+)$/;
420
+ const purposeRe = /^\s+purpose:\s*["']?(.+?)["']?\s*$/;
421
+ const lines = yaml.split("\n");
422
+ let current = null;
423
+ for (const line of lines) {
424
+ if (line.trimStart().startsWith("#")) continue;
425
+ const connMatch = connectorRe.exec(line);
426
+ if (connMatch) {
427
+ if (current?.connector && current.purpose) items.push(current);
428
+ current = { connector: (connMatch[1] ?? "").trim() };
429
+ continue;
430
+ }
431
+ if (current) {
432
+ const purposeMatch = purposeRe.exec(line);
433
+ if (purposeMatch) current.purpose = (purposeMatch[1] ?? "").trim();
434
+ }
399
435
  }
400
- return { body, headline, capabilities };
436
+ if (current?.connector && current.purpose) items.push(current);
437
+ return items;
401
438
  }
402
439
  async function pushSkill(slug, dir) {
403
- const { body, headline, capabilities } = await readSkillDir(dir);
440
+ const { body, headline, capabilities, connections } = await readSkillDir(dir);
404
441
  await apiRequest(`/v1/skills/${slug}/draft`, {
405
442
  method: "PUT",
406
443
  body: JSON.stringify({
407
444
  body,
408
445
  ...headline ? { headline } : {},
409
- ...capabilities ? { capabilities } : {}
446
+ ...capabilities ? { capabilities } : {},
447
+ ...connections ? { connections } : {}
410
448
  })
411
449
  });
412
450
  console.log(`Pushed '${slug}' draft`);
413
451
  }
452
+ function resolveSkillDir(slug) {
453
+ if (!slug) return process.cwd();
454
+ const installedPath = path3.join(CLAUDE_SKILLS_DIR, slug);
455
+ if (existsSync(installedPath)) return installedPath;
456
+ return path3.join(process.cwd(), slug);
457
+ }
414
458
  function registerPushCommand(program2) {
415
- 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) => {
459
+ 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) => {
416
460
  requireAuth();
417
461
  const resolvedSlug = slug ?? path3.basename(process.cwd());
418
- const dir = opts.dir ?? (slug ? path3.join(process.cwd(), slug) : process.cwd());
462
+ const dir = opts.dir ?? resolveSkillDir(slug);
419
463
  await pushSkill(resolvedSlug, dir);
420
464
  if (opts.watch) {
421
465
  console.log(`Watching ${dir} for changes...`);
@@ -469,6 +513,7 @@ headways skills push <slug> # push local edits as a draft
469
513
  SKILL.md # skill body \u2014 instructions for the AI agent
470
514
  headways.yaml # metadata: slug, name, headline, channel, runtimes
471
515
  capabilities.yaml # what the skill is allowed to do
516
+ connections.yaml # MCP connectors this skill requires (optional)
472
517
  hooks.yaml # structured hooks the skill exposes (optional)
473
518
  \`\`\`
474
519
 
@@ -510,6 +555,20 @@ data_classes: none # none | pii | phi | pci
510
555
  auto_send: false # true = skill may act without user confirmation
511
556
  \`\`\`
512
557
 
558
+ ### connections.yaml (required for any skill that uses MCP connector tools)
559
+
560
+ Declare every MCP connector the skill depends on. Users see this list on \`headways skills accept\`
561
+ and the Headways app gates installation on the connectors being configured.
562
+
563
+ \`\`\`yaml
564
+ - connector: slack # connector identifier (e.g. slack, github, atlassian, linear, notion, google-drive, stripe, asana, hubspot, datadog)
565
+ purpose: Read channel messages and threads via Slack MCP tools
566
+ - connector: github
567
+ purpose: Read pull requests and issues
568
+ \`\`\`
569
+
570
+ Omit the file entirely if the skill has no connector dependencies.
571
+
513
572
  ### hooks.yaml (omit if unused)
514
573
 
515
574
  \`\`\`yaml
@@ -565,6 +624,7 @@ local edits with \`headways skills push <slug>\`.
565
624
  - Headline > 90 chars \u2192 submit blocked with 422. Shorten before pushing.
566
625
  - Uppercase or special chars in slug \u2192 rejected at creation. Use \`a-z\`, \`0-9\`, \`-\` only.
567
626
  - Missing \`capabilities.yaml\` entries \u2192 skill silently blocked at runtime.
627
+ - 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.
568
628
  - Passive or noun-phrase headline \u2192 poor discoverability; rewrite as a verb phrase.
569
629
  `.trim();
570
630
  function registerSkillsCommands(program2) {
@@ -576,8 +636,8 @@ function registerSkillsCommands(program2) {
576
636
  console.log(SKILLS_GUIDE);
577
637
  });
578
638
  skills.command("list").description("List skills in the active org").action(async () => {
579
- const { requireAuth: requireAuth2 } = await import("./config-SHMIVRAP.js");
580
- const { apiRequest: apiRequest2 } = await import("./api-2BK6MGZB.js");
639
+ const { requireAuth: requireAuth2 } = await import("./config-XQHAXREA.js");
640
+ const { apiRequest: apiRequest2 } = await import("./api-5EKGGFQ6.js");
581
641
  requireAuth2();
582
642
  const result = await apiRequest2("/v1/skills");
583
643
  if (result.data.length === 0) {
@@ -589,8 +649,25 @@ function registerSkillsCommands(program2) {
589
649
  }
590
650
  });
591
651
  skills.command("accept <slug>").description("Accept a pending skill update and install it locally").action(async (slug) => {
592
- const { acceptSkill } = await import("./sync-6PKI35ZY.js");
652
+ const { acceptSkill } = await import("./sync-Q3OQUWOD.js");
593
653
  await acceptSkill(slug);
654
+ try {
655
+ const { apiRequest: apiRequest2 } = await import("./api-5EKGGFQ6.js");
656
+ const metadata = await apiRequest2(`/v1/skills/${slug}/bundle/metadata`);
657
+ const reqs = metadata.connectionRequirements ?? [];
658
+ if (reqs.length > 0) {
659
+ console.log("");
660
+ console.log("This skill requires the following connectors:");
661
+ console.log("");
662
+ for (const req of reqs) {
663
+ console.log(` - ${req.connector.padEnd(20)} ${req.purpose}`);
664
+ }
665
+ console.log("");
666
+ console.log("To authorize these connectors, use the Headways desktop app");
667
+ console.log("or run: headways connections add <provider>");
668
+ }
669
+ } catch {
670
+ }
594
671
  });
595
672
  skills.command("feedback <slug>").description("Submit feedback about a skill").option(
596
673
  "--reaction <type>",
@@ -616,6 +693,28 @@ function registerSkillsCommands(program2) {
616
693
  });
617
694
  }
618
695
 
696
+ // src/commands/connections/index.ts
697
+ import "commander";
698
+ function registerConnectionsCommands(program2) {
699
+ const connections = program2.command("connections").description("Manage connector authorizations");
700
+ connections.command("add [provider]").description("Authorize a connector (opens the desktop app)").action((provider) => {
701
+ const target = provider ? `the ${provider} connector` : "connectors";
702
+ console.log(`To authorize ${target}, open the Headways desktop app.`);
703
+ console.log("");
704
+ console.log(" 1. Open Headways");
705
+ console.log(" 2. Go to Settings \u2192 Connections");
706
+ console.log(' 3. Click "Connect" next to the connector you want to authorize');
707
+ console.log("");
708
+ console.log(
709
+ "Alternatively, install a skill that requires the connector and it will be set up automatically."
710
+ );
711
+ });
712
+ connections.command("list").description("List authorized connections (opens the desktop app)").action(() => {
713
+ console.log("To view your connections, open the Headways desktop app.");
714
+ console.log("Go to Settings \u2192 Connections to see and manage your authorized connectors.");
715
+ });
716
+ }
717
+
619
718
  // src/sdk/emit.ts
620
719
  import "commander";
621
720
  function registerEmitCommand(program2) {
@@ -657,7 +756,7 @@ function registerEmitCommand(program2) {
657
756
  }
658
757
 
659
758
  // src/commands/prime.ts
660
- import { existsSync, readdirSync, readFileSync } from "fs";
759
+ import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
661
760
  import "commander";
662
761
  function registerPrimeCommand(program2) {
663
762
  program2.command("prime").description("Output Headways workflow context for AI coding assistants").action(() => {
@@ -720,6 +819,10 @@ function registerPrimeCommand(program2) {
720
819
  for (const skill of skills) {
721
820
  const runLine = skill.lastRunAt ? `last run ${new Date(skill.lastRunAt).toLocaleDateString()}` : "never run";
722
821
  lines.push(`- **${skill.slug}** v${skill.version} (${skill.runtime}, ${runLine})`);
822
+ if (skill.connectionRequirements.length > 0) {
823
+ const connectors = skill.connectionRequirements.map((r) => r.connector).join(", ");
824
+ lines.push(` - Requires connectors: ${connectors}`);
825
+ }
723
826
  }
724
827
  }
725
828
  lines.push("", "## Skill Files", "");
@@ -729,7 +832,7 @@ function registerPrimeCommand(program2) {
729
832
  });
730
833
  }
731
834
  function getInstalledSkills() {
732
- if (!existsSync(INSTALLED_DIR)) return [];
835
+ if (!existsSync2(INSTALLED_DIR)) return [];
733
836
  try {
734
837
  return readdirSync(INSTALLED_DIR).filter((f) => f.endsWith(".json")).map((f) => {
735
838
  const slug = f.replace(/\.json$/, "");
@@ -739,7 +842,8 @@ function getInstalledSkills() {
739
842
  slug,
740
843
  version: String(raw.version ?? ""),
741
844
  runtime: String(raw.runtime ?? "claude-code"),
742
- lastRunAt: raw.last_run_at ?? null
845
+ lastRunAt: raw.last_run_at ?? null,
846
+ connectionRequirements: Array.isArray(raw.connection_requirements) ? raw.connection_requirements : []
743
847
  };
744
848
  } catch {
745
849
  return null;
@@ -751,9 +855,12 @@ function getInstalledSkills() {
751
855
  }
752
856
 
753
857
  // src/index.ts
754
- program.name("headways").description("Headways CLI \u2014 skill authoring, sync, and runtime SDK").version("0.2.1");
858
+ var require2 = createRequire(import.meta.url);
859
+ var { version } = require2("../package.json");
860
+ program.name("headways").description("Headways CLI \u2014 skill authoring, sync, and runtime SDK").version(version);
755
861
  registerAuthCommands(program);
756
862
  registerSkillsCommands(program);
863
+ registerConnectionsCommands(program);
757
864
  registerSyncCommands(program);
758
865
  registerEmitCommand(program);
759
866
  registerPrimeCommand(program);
@@ -5,8 +5,8 @@ import {
5
5
  registerDevice,
6
6
  registerSyncCommands,
7
7
  writeSyncState
8
- } from "./chunk-OZULVVQC.js";
9
- import "./chunk-T2H7EXOV.js";
8
+ } from "./chunk-XTEQBKIN.js";
9
+ import "./chunk-UUFIIGTZ.js";
10
10
  export {
11
11
  acceptSkill,
12
12
  readSyncState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headways/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "description": "Headways CLI — authoring, sync, and runtime SDK",
6
6
  "license": "MIT",