@indigoai-us/hq-cli 5.103.21 → 5.103.23

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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `hq integrations show | policy | grants | grant | ungrant | access | share |
3
- * unshare | audit | pending | disconnect`.
3
+ * unshare | audit | pending | disconnect | purge`.
4
4
  *
5
5
  * The govern-and-remove half of the lifecycle. Two different permission
6
6
  * surfaces live here and are easy to confuse, so they get separate verbs:
@@ -16,8 +16,8 @@ import readline from "node:readline";
16
16
  import chalk from "chalk";
17
17
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
18
18
  import { getCompanyUid } from "../utils/vault-api.js";
19
- import { IntegrationsCliError, bareProvider, fetchAdminSurface, printJson, resolveConnection, selectConnection, } from "./integrations-core.js";
20
- import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
19
+ import { IntegrationsCliError, bareProvider, fetchConnections, fetchAdminSurface, printJson, revokedConnectionDetails, resolveConnection, selectConnection, } from "./integrations-core.js";
20
+ import { fetchPendingApprovals, getConnectionAccess, mutateConnectionAccess, purgeConnection, setReadSafe, uninstallIntegration, updateGovernance, } from "./integrations-api.js";
21
21
  const WRITE_POLICIES = ["auto-allow", "confirm", "deny"];
22
22
  const PERMISSIONS = ["read", "write", "admin"];
23
23
  /** Plain-English gloss for each write policy, used in every policy readout. */
@@ -97,6 +97,67 @@ function confirm(message) {
97
97
  function connectionLabel(connection) {
98
98
  return connection.installation?.displayName ?? bareProvider(connection.provider);
99
99
  }
100
+ /**
101
+ * The category of match must mirror `selectConnection`: an exact provider id
102
+ * takes precedence over a display name, which takes precedence over its human
103
+ * slug. Unlike ordinary verbs, purge deliberately operates on historical
104
+ * revoked rows, so it must not inherit selectConnection's active-first policy.
105
+ */
106
+ function purgeSelectorMatches(connections, selector) {
107
+ if (selector.connection) {
108
+ return connections.filter((connection) => connection.id === selector.connection);
109
+ }
110
+ if (selector.provider) {
111
+ const want = selector.provider.trim().toLowerCase();
112
+ const wantHumanSlug = humanSlug(selector.provider);
113
+ const providerMatches = connections.filter((connection) => bareProvider(connection.provider).toLowerCase() === want ||
114
+ connection.provider.toLowerCase() === want);
115
+ if (providerMatches.length > 0)
116
+ return providerMatches;
117
+ const displayNameMatches = connections.filter((connection) => connection.installation?.displayName?.trim().toLowerCase() === want);
118
+ if (displayNameMatches.length > 0)
119
+ return displayNameMatches;
120
+ if (wantHumanSlug) {
121
+ return connections.filter((connection) => {
122
+ const displayName = connection.installation?.displayName;
123
+ return displayName !== undefined && humanSlug(displayName) === wantHumanSlug;
124
+ });
125
+ }
126
+ return [];
127
+ }
128
+ return connections;
129
+ }
130
+ function humanSlug(value) {
131
+ return value
132
+ .trim()
133
+ .toLowerCase()
134
+ .replace(/[^a-z0-9]+/g, "-")
135
+ .replace(/^-+|-+$/g, "");
136
+ }
137
+ function purgeSelector(app, opts) {
138
+ if (app && !opts.provider && !opts.connection) {
139
+ return app.startsWith("acct_") ? { connection: app } : { provider: app };
140
+ }
141
+ return opts;
142
+ }
143
+ async function resolveRevokedPurgeConnection(token, companyUid, app, opts) {
144
+ const selector = purgeSelector(app, opts);
145
+ const connections = await fetchConnections(token, companyUid);
146
+ const matches = purgeSelectorMatches(connections, selector);
147
+ const revoked = matches.filter((connection) => connection.status === "revoked");
148
+ if (revoked.length === 1)
149
+ return revoked[0];
150
+ if (revoked.length > 1) {
151
+ const selected = selector.connection ?? selector.provider ?? "the supplied selector";
152
+ throw new IntegrationsCliError(`Multiple revoked connections match '${selected}'. Use --connection <acct_id> to choose one:\n` +
153
+ revoked.map((connection) => ` --connection ${connection.id} (${connectionLabel(connection)})`).join("\n"), { expected: true });
154
+ }
155
+ if (matches.length > 0) {
156
+ throw new IntegrationsCliError(`${connectionLabel(matches[0])} is still connected. Disconnect it first, then purge it.`, { expected: true });
157
+ }
158
+ // Keep the existing command group's not-found wording and error taxonomy.
159
+ return selectConnection(connections, selector);
160
+ }
100
161
  /**
101
162
  * Governance writes are owner-only on hq-pro AND the allowlist PATCH is a
102
163
  * whole-list REPLACE. A non-owner reads an identity-redacted grant list, so
@@ -144,7 +205,9 @@ export function registerManageCommands(integrations) {
144
205
  ? selectConnection(surface.connections, app.startsWith("acct_") ? { connection: app } : { provider: app })
145
206
  : selectConnection(surface.connections, opts);
146
207
  if (opts.json) {
147
- printJson(connection);
208
+ printJson(connection.status === "revoked"
209
+ ? { ...connection, ...revokedConnectionDetails(connection, opts.company) }
210
+ : connection);
148
211
  return;
149
212
  }
150
213
  const install = connection.installation;
@@ -153,6 +216,11 @@ export function registerManageCommands(integrations) {
153
216
  if (install?.id)
154
217
  console.log(chalk.dim(` installation: ${install.id}`));
155
218
  console.log(chalk.dim(` status: ${connection.status}`));
219
+ if (connection.status === "revoked") {
220
+ const details = revokedConnectionDetails(connection, opts.company);
221
+ console.log(chalk.yellow(` Revoked — ${details.reason}`));
222
+ console.log(chalk.yellow(` Re-add: ${details.fixPath}`));
223
+ }
156
224
  if (install?.domain)
157
225
  console.log(chalk.dim(` domain: ${install.domain}`));
158
226
  if (install?.surface?.url)
@@ -218,6 +286,47 @@ export function registerManageCommands(integrations) {
218
286
  }
219
287
  console.log(chalk.green(`${connectionLabel(connection)}: ${writePolicy} — ${POLICY_BLURB[writePolicy]}`));
220
288
  });
289
+ integrations
290
+ .command("read-safe <tool>")
291
+ .description("Mark a read-only tool as safe to run without approval (owner only)")
292
+ .option("--company <slug>", "Company slug, e.g. indigo")
293
+ .option("--provider <slug>", "Connected app (e.g. linear)")
294
+ .option("--connection <id>", "Connection id (acct_…)")
295
+ .option("--unset", "Remove the read-safe override")
296
+ .option("--off", "Alias for --unset")
297
+ .option("--json", "Machine-readable output")
298
+ .action(async (tool, opts) => {
299
+ const toolName = tool.trim();
300
+ if (!toolName) {
301
+ throw new IntegrationsCliError("Tool name cannot be empty.", { expected: true });
302
+ }
303
+ const token = await ensureCognitoIdToken();
304
+ const companyUid = await getCompanyUid(token, opts.company);
305
+ const connection = await resolveConnection(token, companyUid, undefined, opts);
306
+ const readSafe = !(opts.unset || opts.off);
307
+ try {
308
+ await setReadSafe(token, companyUid, connection.id, toolName, readSafe);
309
+ }
310
+ catch (error) {
311
+ if (error instanceof IntegrationsCliError) {
312
+ if (error.status === 403) {
313
+ throw new IntegrationsCliError("This command is owner only. Ask a company owner to manage read-safe tools.", { expected: true });
314
+ }
315
+ if (readSafe && (error.status === 409 || error.status === 422)) {
316
+ throw new IntegrationsCliError(`${toolName} is a write tool and can't be marked read-safe (its changes always need approval).`, { expected: true });
317
+ }
318
+ }
319
+ throw error;
320
+ }
321
+ const outcome = { connectionId: connection.id, toolName, readSafe };
322
+ if (opts.json) {
323
+ printJson(outcome);
324
+ return;
325
+ }
326
+ console.log(chalk.green(readSafe
327
+ ? `${toolName} on ${connectionLabel(connection)} is now read-safe and skips approval.`
328
+ : `${toolName} on ${connectionLabel(connection)} is no longer read-safe; its override was removed.`));
329
+ });
221
330
  integrations
222
331
  .command("grants [app]")
223
332
  .description("Show who is authorized to run an app's change-making tools")
@@ -518,6 +627,52 @@ export function registerManageCommands(integrations) {
518
627
  }
519
628
  console.log(chalk.green(`Disconnected ${connectionLabel(connection)}.`));
520
629
  });
630
+ integrations
631
+ .command("purge [app]")
632
+ .description("Permanently remove a revoked connection from the list (owner only)")
633
+ .option("--company <slug>", "Company slug, e.g. indigo")
634
+ .option("--provider <slug>", "Connected app (e.g. linear)")
635
+ .option("--connection <id>", "Connection id (acct_…)")
636
+ .option("--yes", "Skip the permanent-removal confirmation prompt")
637
+ .option("--json", "Machine-readable output")
638
+ .action(async (app, opts) => {
639
+ const token = await ensureCognitoIdToken();
640
+ const companyUid = await getCompanyUid(token, opts.company);
641
+ const connection = await resolveRevokedPurgeConnection(token, companyUid, app, opts);
642
+ const label = connectionLabel(connection);
643
+ if (!opts.yes) {
644
+ // Warning + prompt go to stderr: with --json, stdout must stay parseable.
645
+ console.error(`Purging ${chalk.bold(label)} permanently removes its revoked connection record. It cannot be restored.`);
646
+ const ok = await confirm(`Permanently purge ${label}?`);
647
+ if (!ok) {
648
+ // A non-TTY run lands here too: refusing is the only safe default
649
+ // when nobody can confirm a permanent deletion.
650
+ throw new IntegrationsCliError("Not purged. Re-run with --yes if you are sure.", { expected: true });
651
+ }
652
+ }
653
+ try {
654
+ await purgeConnection(token, companyUid, connection.id);
655
+ }
656
+ catch (error) {
657
+ if (error instanceof IntegrationsCliError && error.status === 409) {
658
+ throw new IntegrationsCliError(`${label} is still connected. Disconnect it first, then purge it.`, { expected: true, status: 409, code: error.code });
659
+ }
660
+ if (error instanceof IntegrationsCliError && error.status === 403) {
661
+ throw new IntegrationsCliError("Purging a connection is owner only. Ask a company owner to run this.", { expected: true, status: 403, code: error.code });
662
+ }
663
+ throw error;
664
+ }
665
+ const outcome = {
666
+ connectionId: connection.id,
667
+ provider: bareProvider(connection.provider),
668
+ status: "purged",
669
+ };
670
+ if (opts.json) {
671
+ printJson(outcome);
672
+ return;
673
+ }
674
+ console.log(chalk.green(`Purged ${label}. It will no longer appear in \`hq integrations list\`.`));
675
+ });
521
676
  }
522
677
  /** Order-insensitive identity of an allowlist, for change detection. */
523
678
  export function allowlistFingerprint(grants) {
@@ -26,10 +26,12 @@
26
26
  *
27
27
  * Govern and remove:
28
28
  * hq integrations policy [app] --set <m> Approval setting for changes.
29
+ * hq integrations read-safe <tool> Mark/unmark a trusted read-only tool.
29
30
  * hq integrations grants|grant|ungrant Per-tool authorization to run change-making tools.
30
31
  * hq integrations access|share|unshare Who may use the app.
31
32
  * hq integrations audit Recent activity.
32
33
  * hq integrations disconnect [app] Remove it and its credentials.
34
+ * hq integrations purge [app] Permanently remove a revoked connection row.
33
35
  *
34
36
  * Governance: reads flow freely; calls that can change the app are subject to
35
37
  * the connection's write policy (default: a person approves first). A queued
@@ -26,10 +26,12 @@
26
26
  *
27
27
  * Govern and remove:
28
28
  * hq integrations policy [app] --set <m> Approval setting for changes.
29
+ * hq integrations read-safe <tool> Mark/unmark a trusted read-only tool.
29
30
  * hq integrations grants|grant|ungrant Per-tool authorization to run change-making tools.
30
31
  * hq integrations access|share|unshare Who may use the app.
31
32
  * hq integrations audit Recent activity.
32
33
  * hq integrations disconnect [app] Remove it and its credentials.
34
+ * hq integrations purge [app] Permanently remove a revoked connection row.
33
35
  *
34
36
  * Governance: reads flow freely; calls that can change the app are subject to
35
37
  * the connection's write policy (default: a person approves first). A queued
@@ -50,11 +52,65 @@ import { randomUUID } from "node:crypto";
50
52
  import chalk from "chalk";
51
53
  import { ensureCognitoIdToken } from "../utils/cognito-session.js";
52
54
  import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
53
- import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
55
+ import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, isClientError, printJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
54
56
  import { registerConnectCommands } from "./integrations-connect.js";
55
57
  import { registerImportCommands } from "./integrations-import.js";
56
58
  import { registerManageCommands } from "./integrations-manage.js";
57
59
  export { IntegrationsCliError, callGateway, fetchConnections, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
60
+ /**
61
+ * Gateway metadata is additive and may be absent when talking to an older
62
+ * gateway. Parse it defensively so an unknown or malformed value never breaks
63
+ * tool discovery.
64
+ */
65
+ function effectiveToolMode(tool) {
66
+ const raw = tool._meta?.["hq/effective-mode"];
67
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
68
+ return null;
69
+ const mode = raw.mode;
70
+ const approvalRequired = raw.approvalRequired;
71
+ if ((mode !== "read" && mode !== "write") || typeof approvalRequired !== "boolean") {
72
+ return null;
73
+ }
74
+ const denied = raw.denied;
75
+ const policy = raw.policy;
76
+ return {
77
+ mode,
78
+ approvalRequired,
79
+ ...(denied === true ? { denied: true } : {}),
80
+ ...(policy === "deny" ? { policy: "deny" } : {}),
81
+ };
82
+ }
83
+ function toolBadge(tool) {
84
+ const effectiveMode = effectiveToolMode(tool);
85
+ if (!effectiveMode)
86
+ return null;
87
+ if (effectiveMode.mode === "read")
88
+ return "read";
89
+ if (effectiveMode.denied)
90
+ return "denied";
91
+ return effectiveMode.approvalRequired ? "approval-required" : "write";
92
+ }
93
+ function formatToolBadge(tool) {
94
+ const badge = toolBadge(tool);
95
+ if (!badge)
96
+ return "";
97
+ const label = `[${badge}]`;
98
+ switch (badge) {
99
+ case "read":
100
+ return chalk.green(label);
101
+ case "approval-required":
102
+ return chalk.yellow(label);
103
+ case "denied":
104
+ return chalk.red(label);
105
+ case "write":
106
+ return chalk.blue(label);
107
+ }
108
+ }
109
+ /** Add gateway policy metadata where scripts can consume it directly. */
110
+ function toolJsonView(tool) {
111
+ const effectiveMode = effectiveToolMode(tool);
112
+ return effectiveMode ? { ...tool, effectiveMode } : tool;
113
+ }
58
114
  /**
59
115
  * The gateway relays MCP tool schemas verbatim. Keep the default listing
60
116
  * compact, but make required inputs visible before a caller has to discover
@@ -86,6 +142,23 @@ function printToolSchema(tool) {
86
142
  console.log(chalk.dim(" Input schema:"));
87
143
  console.log(chalk.dim(schema.split("\n").map((line) => ` ${line}`).join("\n")));
88
144
  }
145
+ function reportRevokedConnection(connection, opts) {
146
+ if (connection.status !== "revoked")
147
+ return false;
148
+ const details = revokedConnectionDetails(connection, opts.company);
149
+ if (opts.json) {
150
+ printJson(details);
151
+ }
152
+ else {
153
+ console.log(chalk.yellow(`${connectionLabel(connection)}: status=revoked`));
154
+ console.log(chalk.yellow(` ${details.reason}`));
155
+ console.log(` Re-add: ${details.fixPath}`);
156
+ }
157
+ if (opts.fail) {
158
+ throw new IntegrationsCliError(`status=revoked. ${details.reason} Re-add with: ${details.fixPath}`, { expected: true });
159
+ }
160
+ return true;
161
+ }
89
162
  export function registerIntegrationsCommand(program) {
90
163
  const integrations = program
91
164
  .command("integrations")
@@ -132,6 +205,8 @@ export function registerIntegrationsCommand(program) {
132
205
  const token = await ensureCognitoIdToken();
133
206
  const companyUid = await getCompanyUid(token, opts.company);
134
207
  const connection = selectConnection(await fetchConnections(token, companyUid), opts);
208
+ if (reportRevokedConnection(connection, opts))
209
+ return;
135
210
  const prefix = toolPrefixForProvider(connection.provider);
136
211
  const message = await callGateway(token, {
137
212
  companyUid,
@@ -154,24 +229,26 @@ export function registerIntegrationsCommand(program) {
154
229
  throw new IntegrationsCliError(`No tool named '${opts.describe}' on ${connectionLabel(connection)}.`, { expected: true });
155
230
  }
156
231
  if (opts.json) {
157
- printJson(tool);
232
+ printJson(toolJsonView(tool));
158
233
  return;
159
234
  }
160
- console.log(`${chalk.bold(tool.name)} ${chalk.dim(`on ${connectionLabel(connection)}`)}`);
235
+ const badge = formatToolBadge(tool);
236
+ console.log(`${chalk.bold(tool.name)}${badge ? ` ${badge}` : ""} ${chalk.dim(`on ${connectionLabel(connection)}`)}`);
161
237
  if (tool.description)
162
238
  console.log(chalk.dim(` ${tool.description}`));
163
239
  printToolSchema(tool);
164
240
  return;
165
241
  }
166
242
  if (opts.json) {
167
- printJson(payload);
243
+ printJson({ ...payload, tools: tools.map(toolJsonView) });
168
244
  return;
169
245
  }
170
246
  console.log(chalk.dim(`App: ${connectionLabel(connection)}`));
171
247
  for (const tool of tools) {
172
248
  const label = tool.title && tool.title !== tool.name ? ` ${chalk.dim(tool.title)}` : "";
173
249
  const required = requiredInputHint(tool.inputSchema);
174
- console.log(`${chalk.bold(tool.name)}${label}${required ? ` ${chalk.dim(required)}` : ""}`);
250
+ const badge = formatToolBadge(tool);
251
+ console.log(`${chalk.bold(tool.name)}${badge ? ` ${badge}` : ""}${label}${required ? ` ${chalk.dim(required)}` : ""}`);
175
252
  }
176
253
  console.log(chalk.dim(`\n${tools.length} tools. Describe inputs: hq integrations tools --provider ${bareProvider(connection.provider)} --describe <tool>\nCall one with: hq integrations call <tool> --provider ${bareProvider(connection.provider)} --args '<json>'`));
177
254
  });
@@ -199,6 +276,8 @@ export function registerIntegrationsCommand(program) {
199
276
  const token = await ensureCognitoIdToken();
200
277
  const companyUid = await getCompanyUid(token, opts.company);
201
278
  const connection = selectConnection(await fetchConnections(token, companyUid), opts);
279
+ if (reportRevokedConnection(connection, { ...opts, fail: true }))
280
+ return;
202
281
  const prefix = toolPrefixForProvider(connection.provider);
203
282
  const idempotencyKey = opts.idempotencyKey ?? `hq-cli-${randomUUID()}`;
204
283
  const message = await callGateway(token, {
@@ -1,153 +1,55 @@
1
1
  /**
2
- * `hq skill` subcommand group (US-017).
2
+ * Company skill creation and comment-only improvements.
3
3
  *
4
- * Terminal / scriptable access to the skill collaboration loop:
5
- *
6
- * hq skill suggest <uid|path> Propose a change to a skill from working edits.
7
- * hq skill list-suggestions Show the review inbox (suggestions on skills you own).
8
- * hq skill review <sgn_…> Accept (merge) or decline a pending suggestion.
9
- *
10
- * This is a THIN front-end over the SAME wired hq-pro routes the MCP surface
11
- * (US-007) and the console merge path (US-009) use — there is NO forked
12
- * suggestion or merge logic here. The CLI reads the local working SKILL.md,
13
- * computes the proposed content (+ its base for a diff), and POSTs to:
14
- *
15
- * CREATE POST /v1/files/skills/company/{slug}/{skillUid}/suggestions
16
- * LIST POST /v1/files/skills/company/{slug}/suggestions/list
17
- * ACCEPT POST /v1/files/skills/company/{slug}/{skillUid}/suggestions/{id}/accept
18
- * DECLINE POST /v1/files/skills/company/{slug}/{skillUid}/suggestions/{id}/decline
19
- *
20
- * The skill routes are keyed on the company SLUG (path param, resolved
21
- * server-side via findEntityBySlug) — NOT the companyUid the vault/ACL routes
22
- * use — so this module resolves a slug (from `--company` or the active company)
23
- * and passes it straight through.
24
- *
25
- * Lock semantics (AC3): a suggest against a skill the caller cannot WRITE still
26
- * SUCCEEDS as a proposal. The CREATE route is MEMBER-gated (never write-gated),
27
- * so this command performs NO client-side lock/permission pre-check — it always
28
- * posts and renders whatever the server returns. A locked-out member lands a
29
- * suggestion, never a hard permission error.
30
- *
31
- * Attribution (AC4): the invoking identity (from the Cognito JWT) is the server-
32
- * derived `authorPersonUid`; the CLI never sends an author. An optional
33
- * `--note` rides as `authorNote` (the change's rationale / failure context).
4
+ * `hq skill create <slug>` registers a canonical company skill, stamps its
5
+ * immutable UID, reindexes its generated runtime wrapper, and syncs it.
6
+ * `hq skill propose <uid|path> --message "…"` posts a whole-skill comment to
7
+ * the same improvement thread shown in HQ Console. It never uploads a modified
8
+ * SKILL.md and cannot overwrite live content. Structured suggest/list/review
9
+ * commands intentionally are not registered.
34
10
  */
35
11
  import { Command } from "commander";
36
- /**
37
- * A `skl_…` argument to `suggest` is a skill UID (resolve the local file from
38
- * it); anything else is a filesystem path. Lenient on the suffix (the strict
39
- * `skl_<ulid>` shape is `isSkillUid` on the server) so a hand-typed / fixture
40
- * uid still routes to the uid branch.
41
- */
12
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
13
+ import { vaultApiFetch } from "../utils/vault-api.js";
42
14
  export declare const SKILL_UID_PATTERN: RegExp;
43
- /** A `sgn_…` suggestion id — the `review` target and the LIST row key. */
44
- export declare const SUGGESTION_ID_PATTERN: RegExp;
45
- /**
46
- * Read the top-level `skill_uid` from a SKILL.md's YAML frontmatter. Mirrors
47
- * hq-pro's `parseSkillFrontmatter` (block extraction → YAML parse → read the
48
- * top-level `skill_uid` string). Returns `undefined` when there is no
49
- * frontmatter, it fails to parse, or `skill_uid` is absent / not a `skl_…`
50
- * string. Never throws.
51
- */
52
- export declare function parseSkillUid(md: string): string | undefined;
53
- /** sha256 hex of a string — the base-version fingerprint the server records (AC2). */
54
- export declare function sha256Hex(content: string): string;
55
- export interface SuggestionCreateBody {
56
- proposedContent: string;
57
- baseContent?: string;
58
- baseContentHash?: string;
59
- authorNote?: string;
15
+ export declare const SKILL_SLUG_PATTERN: RegExp;
16
+ interface SkillSyncInput {
17
+ filePath: string;
18
+ companySlug: string;
19
+ hqRoot: string;
20
+ token: string;
60
21
  }
61
- /**
62
- * Build the CREATE request body from the resolved proposed content and an
63
- * optional base. Mirrors the server's two payload shapes:
64
- * - `baseContent` present → the server derives the unified diff + base hash
65
- * (the diff-by-default path). A no-op (base === proposed) is rejected here
66
- * rather than round-tripped to an EmptySuggestion 400.
67
- * - no base → a `full-file` proposal; the server REQUIRES a `baseContentHash`,
68
- * so we fingerprint the proposed content (a "here is my whole file" proposal
69
- * with no base to diff against).
70
- * An empty / whitespace-only note is dropped (kept absent, not blank).
71
- */
72
- export declare function buildSuggestionCreateBody(input: {
73
- proposedContent: string;
74
- baseContent?: string;
75
- note?: string;
76
- }): SuggestionCreateBody;
77
- /**
78
- * Map an hq-pro skill route error to a single user-facing line. Pure so the
79
- * status → copy mapping is unit-tested independently of the network. Prefers the
80
- * server's own `error` / `message` (they carry the actionable specifics — e.g.
81
- * "Skill not found", "You need write access to this skill…").
82
- */
83
- export declare function mapSkillError(status: number, body: Record<string, unknown>): string;
84
- /** LIST inbox row shape returned by `suggestionToWire` on the server. */
85
- export interface SuggestionRow {
86
- suggestionId: string;
87
- skillUid: string;
88
- authorPersonUid: string;
89
- status: string;
90
- baseChanged: boolean;
91
- presentation?: string;
92
- unifiedDiff?: string;
93
- fullContent?: string;
94
- baseContentHash?: string;
95
- currentContentHash?: string;
96
- authorNote?: string;
97
- createdAt: string;
98
- path: string;
22
+ interface SkillSyncResult {
23
+ filesUploaded: number;
24
+ filesSkipped: number;
25
+ aborted: boolean;
99
26
  }
100
- /**
101
- * Render the review inbox as a table (one row per suggestion), optionally
102
- * printing each suggestion's unified diff (or full proposed file, when the base
103
- * drifted / the proposal is full-file) beneath its row. Pure → snapshot-testable.
104
- */
105
- export declare function formatSuggestionsList(suggestions: SuggestionRow[], opts?: {
106
- showDiff?: boolean;
107
- }): string;
108
- /** Read `.hq/config.json`'s `activeCompany` (mirrors signals/sources). */
27
+ export declare function parseSkillUid(markdown: string): string | undefined;
109
28
  export declare function readActiveCompanySlug(hqRoot: string): string | undefined;
110
- /**
111
- * The skill routes are keyed on the company SLUG. Precedence: explicit
112
- * `--company` → `.hq/config.json` activeCompany. Throws with actionable copy
113
- * when neither is available.
114
- */
115
29
  export declare function resolveCompanySlug(flag: string | undefined, hqRoot?: string): string;
116
- export interface ResolvedSkillTarget {
117
- /** Absolute path to the SKILL.md. */
118
- filePath: string;
119
- /** The `skl_…` uid read from its frontmatter (or the uid arg). */
120
- skillUid: string;
121
- /** The working-tree SKILL.md content — the proposed content. */
122
- content: string;
30
+ /** Resolve a UID directly, or read the immutable UID from a local SKILL.md. */
31
+ export declare function resolveSkillUid(target: string, cwd: string): string;
32
+ export declare function canonicalCompanySkillPath(hqRoot: string, companySlug: string, skillSlug: string): string;
33
+ export declare function makeSkillTemplate(input: {
34
+ slug: string;
35
+ name?: string;
36
+ description?: string;
37
+ }): string;
38
+ /** Replace SKILL.md without exposing a partially-written identity to agents. */
39
+ export declare function writeSkillFileAtomically(filePath: string, content: string): void;
40
+ export declare function mapSkillError(status: number, body: Record<string, unknown>): string;
41
+ interface SkillCommandDeps {
42
+ ensureToken?: typeof ensureCognitoToken;
43
+ apiFetch?: typeof vaultApiFetch;
44
+ cwd?: () => string;
45
+ hqRoot?: string;
46
+ syncFile?: (input: SkillSyncInput) => Promise<SkillSyncResult>;
47
+ reindexFn?: (input: {
48
+ repoRoot: string;
49
+ }) => {
50
+ status: number | null;
51
+ };
123
52
  }
124
- /**
125
- * Recursively find a SKILL.md whose frontmatter `skill_uid` equals `uid`,
126
- * searching each root breadth-first with a bounded depth (skips VCS / build /
127
- * dependency dirs). Returns the first match's absolute path, or null.
128
- */
129
- export declare function findSkillFileByUid(roots: string[], uid: string, maxDepth?: number): string | null;
130
- /**
131
- * Resolve a `suggest` target (a `skl_…` uid OR a filesystem path) to the local
132
- * SKILL.md, its uid, and its content. A uid is resolved by scanning the company
133
- * skills dir and the cwd; a path is read directly (a directory → its SKILL.md),
134
- * with the uid read from the file's frontmatter.
135
- */
136
- export declare function resolveSkillTarget(target: string, deps: {
137
- cwd: string;
138
- hqRoot: string;
139
- companySlug: string;
140
- }): ResolvedSkillTarget;
141
- /**
142
- * Read the committed (HEAD) version of a file from its git repo — the diff base
143
- * for "propose my working changes". Returns null when the file is untracked, not
144
- * in a repo, or git is unavailable (the caller then falls back to full-file, or
145
- * errors under `--diff`). Never throws.
146
- */
147
- export declare function readGitBase(filePath: string): Promise<string | null>;
148
- /** Injectable git-base seam so `suggest` is testable without a real repo. */
149
- export type GitBaseReader = (filePath: string) => Promise<string | null>;
150
- export declare function registerSkillCommand(program: Command, deps?: {
151
- gitBase?: GitBaseReader;
152
- }): Command;
53
+ export declare function registerSkillCommand(program: Command, deps?: SkillCommandDeps): Command;
54
+ export {};
153
55
  //# sourceMappingURL=skill.d.ts.map