@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.
- package/CHANGELOG.md +15 -0
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +2 -2
- package/dist/commands/integrations-api.d.ts +13 -0
- package/dist/commands/integrations-api.js +35 -0
- package/dist/commands/integrations-connect.js +79 -42
- package/dist/commands/integrations-core.d.ts +20 -0
- package/dist/commands/integrations-core.js +59 -5
- package/dist/commands/integrations-manage.d.ts +1 -1
- package/dist/commands/integrations-manage.js +159 -4
- package/dist/commands/integrations.d.ts +2 -0
- package/dist/commands/integrations.js +84 -5
- package/dist/commands/skill.d.ts +44 -142
- package/dist/commands/skill.js +214 -521
- package/dist/main.js +2 -3
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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, {
|
package/dist/commands/skill.d.ts
CHANGED
|
@@ -1,153 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Company skill creation and comment-only improvements.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|