@indigoai-us/hq-cli 5.115.6 → 5.117.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.
Files changed (76) hide show
  1. package/CHANGELOG.md +169 -11
  2. package/dist/command-catalog.generated.d.ts +220 -2
  3. package/dist/command-catalog.generated.js +281 -2
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/agent-enroll.d.ts +105 -0
  7. package/dist/commands/agent-enroll.js +273 -0
  8. package/dist/commands/agent-kit.d.ts +53 -0
  9. package/dist/commands/agent-kit.js +260 -0
  10. package/dist/commands/agent-mcp.d.ts +22 -0
  11. package/dist/commands/agent-mcp.js +104 -0
  12. package/dist/commands/agent-probe.d.ts +71 -0
  13. package/dist/commands/agent-probe.js +294 -0
  14. package/dist/commands/agent.d.ts +12 -0
  15. package/dist/commands/agent.js +23 -0
  16. package/dist/commands/agents.d.ts +27 -0
  17. package/dist/commands/agents.js +280 -6
  18. package/dist/commands/bot.d.ts +140 -1
  19. package/dist/commands/bot.js +757 -22
  20. package/dist/commands/secrets.js +17 -5
  21. package/dist/lib/agent-kit/creds.d.ts +60 -0
  22. package/dist/lib/agent-kit/creds.js +123 -0
  23. package/dist/lib/agent-kit/kit-config.d.ts +29 -0
  24. package/dist/lib/agent-kit/kit-config.js +54 -0
  25. package/dist/lib/agent-kit/log.d.ts +17 -0
  26. package/dist/lib/agent-kit/log.js +46 -0
  27. package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +84 -0
  28. package/dist/lib/agent-kit/mcp/jsonrpc.js +164 -0
  29. package/dist/lib/agent-kit/mcp/tools.d.ts +45 -0
  30. package/dist/lib/agent-kit/mcp/tools.js +280 -0
  31. package/dist/lib/agent-kit/paths.d.ts +42 -0
  32. package/dist/lib/agent-kit/paths.js +56 -0
  33. package/dist/lib/agent-kit/run/heartbeat.d.ts +52 -0
  34. package/dist/lib/agent-kit/run/heartbeat.js +97 -0
  35. package/dist/lib/agent-kit/run/inbox.d.ts +59 -0
  36. package/dist/lib/agent-kit/run/inbox.js +152 -0
  37. package/dist/lib/agent-kit/run/mesh-listener.d.ts +58 -0
  38. package/dist/lib/agent-kit/run/mesh-listener.js +193 -0
  39. package/dist/lib/agent-kit/run/sync.d.ts +33 -0
  40. package/dist/lib/agent-kit/run/sync.js +58 -0
  41. package/dist/lib/agent-kit/services.d.ts +21 -0
  42. package/dist/lib/agent-kit/services.js +46 -0
  43. package/dist/lib/agent-kit/skills.d.ts +18 -0
  44. package/dist/lib/agent-kit/skills.js +149 -0
  45. package/dist/lib/bot/api.d.ts +51 -0
  46. package/dist/lib/bot/api.js +32 -0
  47. package/dist/lib/bot/daemon.d.ts +17 -0
  48. package/dist/lib/bot/daemon.js +44 -3
  49. package/dist/lib/bot/index.d.ts +4 -0
  50. package/dist/lib/bot/index.js +4 -0
  51. package/dist/lib/bot/inflight.d.ts +14 -0
  52. package/dist/lib/bot/local-config.d.ts +70 -0
  53. package/dist/lib/bot/local-config.js +147 -0
  54. package/dist/lib/bot/local-name.d.ts +54 -0
  55. package/dist/lib/bot/local-name.js +114 -0
  56. package/dist/lib/bot/run.d.ts +9 -0
  57. package/dist/lib/bot/run.js +117 -24
  58. package/dist/lib/bot/runnable.d.ts +51 -0
  59. package/dist/lib/bot/runnable.js +65 -0
  60. package/dist/lib/bot/self-heal.d.ts +52 -0
  61. package/dist/lib/bot/self-heal.js +79 -0
  62. package/dist/lib/bot/split.d.ts +32 -0
  63. package/dist/lib/bot/split.js +241 -0
  64. package/dist/lib/service-manager/index.d.ts +43 -0
  65. package/dist/lib/service-manager/index.js +114 -0
  66. package/dist/lib/service-manager/launchd.d.ts +23 -0
  67. package/dist/lib/service-manager/launchd.js +81 -0
  68. package/dist/lib/service-manager/systemd.d.ts +19 -0
  69. package/dist/lib/service-manager/systemd.js +72 -0
  70. package/dist/lib/service-manager/types.d.ts +32 -0
  71. package/dist/lib/service-manager/types.js +26 -0
  72. package/dist/utils/self-update.js +2 -30
  73. package/dist/utils/update-command-supervisor.cjs +194 -0
  74. package/dist/utils/version-gate.d.ts +18 -0
  75. package/dist/utils/version-gate.js +126 -7
  76. package/package.json +2 -2
@@ -43,7 +43,19 @@ export const CLI_SECRETS_SET_SURFACE = "cli_secrets_set";
43
43
  */
44
44
  async function requireCognitoTokenForSecrets(commandLabel, capability = null) {
45
45
  requireApiKeyCapability(capability, commandLabel, { routeAvailable: false });
46
- return ensureCognitoToken();
46
+ return ensureCognitoToken(secretsCredentialOptions());
47
+ }
48
+ /**
49
+ * A browser login is safe only when a person can answer it. This mirrors the
50
+ * repository's existing terminal convention in `canOfferTeamUpgrade`: both
51
+ * stdin and stderr must be TTYs, and CI is always non-interactive.
52
+ */
53
+ function secretsCredentialOptions() {
54
+ return {
55
+ interactive: process.stdin.isTTY === true &&
56
+ process.stderr.isTTY === true &&
57
+ !process.env.CI,
58
+ };
47
59
  }
48
60
  function scopeOpts(opts) {
49
61
  if (opts.personal && opts.company) {
@@ -96,7 +108,7 @@ function resolveApiKeyCompanyUid(scope, commandLabel) {
96
108
  return ref;
97
109
  }
98
110
  async function resolveSecretsTarget(capability, commandLabel, scopeOptions) {
99
- const cred = await resolveVaultCredentialForCapability(capability, commandLabel);
111
+ const cred = await resolveVaultCredentialForCapability(capability, commandLabel, secretsCredentialOptions());
100
112
  const scope = scopeOpts(scopeOptions);
101
113
  const isApiKey = cred.kind === "api-key";
102
114
  const companyUid = isApiKey
@@ -883,7 +895,7 @@ export function registerSecretsCommand(program) {
883
895
  .option("--reveal", "Include the decrypted secret value")
884
896
  .action(async (name, opts) => {
885
897
  try {
886
- const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets get");
898
+ const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets get", secretsCredentialOptions());
887
899
  if (cred.kind === "api-key") {
888
900
  if (!opts.reveal) {
889
901
  console.error(chalk.red("HQ_API_KEY is set; `hq secrets get` without --reveal is not supported for API keys. " +
@@ -1434,7 +1446,7 @@ export function registerSecretsCommand(program) {
1434
1446
  process.exit(1);
1435
1447
  }
1436
1448
  const keys = parseSecretNameList(_opts.only);
1437
- const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets exec");
1449
+ const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets exec", secretsCredentialOptions());
1438
1450
  const companyUid = cred.kind === "api-key"
1439
1451
  ? "__api_key__"
1440
1452
  : await getEntityUid(cred.token, scopeOpts(secrets.opts()));
@@ -1485,7 +1497,7 @@ export function registerSecretsCommand(program) {
1485
1497
  console.error(chalk.yellow("stdout is a terminal — values redacted. Use: source <(hq secrets env --only KEY1,KEY2)"));
1486
1498
  }
1487
1499
  const keys = parseSecretNameList(opts.only);
1488
- const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets env");
1500
+ const cred = await resolveVaultCredentialForCapability("secrets:read", "secrets env", secretsCredentialOptions());
1489
1501
  const companyUid = cred.kind === "api-key"
1490
1502
  ? "__api_key__"
1491
1503
  : await getEntityUid(cred.token, scopeOpts(secrets.opts()));
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Host key + machine-creds file handling for external agents.
3
+ *
4
+ * The private key and the creds file are the whole credential: a stolen
5
+ * creds file without the key mints nothing (hq-cloud signs every mint with
6
+ * the key), so both are written 0600 and never printed. Nothing in this
7
+ * module reads `~/.hq/cognito-tokens.json` — a human's session is never
8
+ * consulted, copied, or reused by an agent identity (policy
9
+ * never-extract-stored-session-tokens-to-forge-privileged-calls).
10
+ */
11
+ import type { AgentKitPaths, KitComponent } from "./paths.js";
12
+ /** Exact `machine-creds.json` shape for an external agent (api-contract v1). */
13
+ export interface ExternalMachineCreds {
14
+ username: string;
15
+ secret: string;
16
+ /** Cognito user pool the machine user lives in (informational for the host). */
17
+ userPoolId?: string;
18
+ clientId: string;
19
+ region: string;
20
+ entityType: "agent";
21
+ entityUid: string;
22
+ runtime: "external";
23
+ hostKeyPath: string;
24
+ companySlug: string;
25
+ apiBaseUrl: string;
26
+ }
27
+ export interface HostKeyPair {
28
+ privatePem: string;
29
+ publicPem: string;
30
+ /** SHA-256 of the DER (SPKI) public key, hex, first 16 chars. */
31
+ fingerprint: string;
32
+ }
33
+ /** Contract fingerprint: sha256(DER SPKI public key) hex, first 16 chars. */
34
+ export declare function hostFingerprint(publicPem: string): string;
35
+ export declare function generateHostKeyPair(): HostKeyPair;
36
+ export declare function writeHostKeyPair(paths: Pick<AgentKitPaths, "hostKeyPath" | "hostKeyPubPath">, pair: HostKeyPair): void;
37
+ export declare function writeMachineCreds(paths: Pick<AgentKitPaths, "machineCredsPath">, creds: ExternalMachineCreds): void;
38
+ /**
39
+ * Read the creds file without trusting it: returns null unless it is an
40
+ * external-agent file with every contract field present.
41
+ */
42
+ export declare function readExternalMachineCreds(paths: Pick<AgentKitPaths, "machineCredsPath">): ExternalMachineCreds | null;
43
+ /** Whether ANY machine-creds file exists (external or hosted) at the kit path. */
44
+ export declare function machineCredsFileExists(paths: Pick<AgentKitPaths, "machineCredsPath">): boolean;
45
+ /**
46
+ * Component health stamp the kit services write and the heartbeat reads:
47
+ * `<status> <iso8601>` — same line shape as the hosted box's component files.
48
+ */
49
+ export type ComponentStatus = "ok" | "error";
50
+ export declare function writeComponentStatus(paths: Pick<AgentKitPaths, "stateDir">, component: KitComponent, status: ComponentStatus, now?: () => Date): void;
51
+ export interface ComponentReading {
52
+ status: ComponentStatus;
53
+ at: Date | null;
54
+ }
55
+ /**
56
+ * Read a component stamp. A missing file, an unparseable line, or a stamp
57
+ * older than `staleAfterMs` reads as `error` — silence is never health.
58
+ */
59
+ export declare function readComponentStatus(paths: Pick<AgentKitPaths, "stateDir">, component: KitComponent, staleAfterMs: number, now?: () => Date): ComponentReading;
60
+ //# sourceMappingURL=creds.d.ts.map
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Host key + machine-creds file handling for external agents.
3
+ *
4
+ * The private key and the creds file are the whole credential: a stolen
5
+ * creds file without the key mints nothing (hq-cloud signs every mint with
6
+ * the key), so both are written 0600 and never printed. Nothing in this
7
+ * module reads `~/.hq/cognito-tokens.json` — a human's session is never
8
+ * consulted, copied, or reused by an agent identity (policy
9
+ * never-extract-stored-session-tokens-to-forge-privileged-calls).
10
+ */
11
+ import { createHash, createPublicKey, generateKeyPairSync } from "node:crypto";
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { componentStatePath } from "./paths.js";
15
+ /** Contract fingerprint: sha256(DER SPKI public key) hex, first 16 chars. */
16
+ export function hostFingerprint(publicPem) {
17
+ const der = createPublicKey(publicPem).export({ type: "spki", format: "der" });
18
+ return createHash("sha256").update(der).digest("hex").slice(0, 16);
19
+ }
20
+ export function generateHostKeyPair() {
21
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
22
+ const privatePem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
23
+ const publicPem = publicKey.export({ type: "spki", format: "pem" }).toString();
24
+ return { privatePem, publicPem, fingerprint: hostFingerprint(publicPem) };
25
+ }
26
+ /** Write a file atomically with the given mode, replacing any existing one. */
27
+ function writePrivateFile(dest, contents, mode) {
28
+ fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
29
+ const tmp = `${dest}.tmp.${process.pid}`;
30
+ fs.writeFileSync(tmp, contents, { mode });
31
+ fs.chmodSync(tmp, mode);
32
+ fs.renameSync(tmp, dest);
33
+ }
34
+ export function writeHostKeyPair(paths, pair) {
35
+ writePrivateFile(paths.hostKeyPath, pair.privatePem, 0o600);
36
+ writePrivateFile(paths.hostKeyPubPath, pair.publicPem, 0o644);
37
+ }
38
+ export function writeMachineCreds(paths, creds) {
39
+ writePrivateFile(paths.machineCredsPath, `${JSON.stringify(creds, null, 2)}\n`, 0o600);
40
+ }
41
+ /**
42
+ * Read the creds file without trusting it: returns null unless it is an
43
+ * external-agent file with every contract field present.
44
+ */
45
+ export function readExternalMachineCreds(paths) {
46
+ let raw;
47
+ try {
48
+ raw = JSON.parse(fs.readFileSync(paths.machineCredsPath, "utf8"));
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ if (!raw || typeof raw !== "object")
54
+ return null;
55
+ const r = raw;
56
+ const str = (k) => typeof r[k] === "string" && r[k].length > 0 ? r[k] : null;
57
+ const username = str("username");
58
+ const secret = str("secret");
59
+ const clientId = str("clientId");
60
+ const region = str("region");
61
+ const entityUid = str("entityUid");
62
+ const userPoolId = str("userPoolId");
63
+ const hostKeyPath = str("hostKeyPath");
64
+ const companySlug = str("companySlug");
65
+ const apiBaseUrl = str("apiBaseUrl");
66
+ if (r.runtime !== "external" ||
67
+ r.entityType !== "agent" ||
68
+ !username ||
69
+ !secret ||
70
+ !clientId ||
71
+ !region ||
72
+ !entityUid ||
73
+ !entityUid.startsWith("agt_") ||
74
+ !hostKeyPath ||
75
+ !companySlug ||
76
+ !apiBaseUrl) {
77
+ return null;
78
+ }
79
+ return {
80
+ username,
81
+ secret,
82
+ ...(userPoolId ? { userPoolId } : {}),
83
+ clientId,
84
+ region,
85
+ entityType: "agent",
86
+ entityUid,
87
+ runtime: "external",
88
+ hostKeyPath,
89
+ companySlug,
90
+ apiBaseUrl,
91
+ };
92
+ }
93
+ /** Whether ANY machine-creds file exists (external or hosted) at the kit path. */
94
+ export function machineCredsFileExists(paths) {
95
+ return fs.existsSync(paths.machineCredsPath);
96
+ }
97
+ export function writeComponentStatus(paths, component, status, now = () => new Date()) {
98
+ fs.mkdirSync(paths.stateDir, { recursive: true, mode: 0o700 });
99
+ fs.writeFileSync(componentStatePath(paths, component), `${status} ${now().toISOString()}\n`, { mode: 0o600 });
100
+ }
101
+ /**
102
+ * Read a component stamp. A missing file, an unparseable line, or a stamp
103
+ * older than `staleAfterMs` reads as `error` — silence is never health.
104
+ */
105
+ export function readComponentStatus(paths, component, staleAfterMs, now = () => new Date()) {
106
+ let line;
107
+ try {
108
+ line = fs.readFileSync(componentStatePath(paths, component), "utf8").trim();
109
+ }
110
+ catch {
111
+ return { status: "error", at: null };
112
+ }
113
+ const [status, iso] = line.split(/\s+/, 2);
114
+ const at = iso ? new Date(iso) : null;
115
+ if (status !== "ok" || !at || Number.isNaN(at.getTime())) {
116
+ return { status: "error", at: at && !Number.isNaN(at.getTime()) ? at : null };
117
+ }
118
+ if (now().getTime() - at.getTime() > staleAfterMs) {
119
+ return { status: "error", at };
120
+ }
121
+ return { status: "ok", at };
122
+ }
123
+ //# sourceMappingURL=creds.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ~/.hq-agent/kit.json — settings `hq agent kit install` records so the
3
+ * services (which only receive `hq agent kit run <service>`) know where to
4
+ * sync and how often. Kept separate from machine-creds.json, which is the
5
+ * contract-shaped credential file and nothing else.
6
+ */
7
+ import type { AgentKitPaths } from "./paths.js";
8
+ export declare const DEFAULT_SYNC_INTERVAL_MS: number;
9
+ export declare const DEFAULT_INBOX_POLL_MS = 15000;
10
+ export declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
11
+ export declare const DEFAULT_MESH_REFRESH_MS: number;
12
+ export interface KitConfig {
13
+ /** Local HQ tree the company vault syncs into (companies/<slug>/ …). */
14
+ hqRoot: string;
15
+ syncIntervalMs: number;
16
+ inboxPollMs: number;
17
+ heartbeatIntervalMs: number;
18
+ meshRefreshMs: number;
19
+ /** Ack mirrored inbox items on the server after writing them locally. */
20
+ inboxAck: boolean;
21
+ installedAt: string;
22
+ cliVersion: string;
23
+ }
24
+ export declare function defaultHqRoot(paths: Pick<AgentKitPaths, "agentDir">): string;
25
+ export declare function defaultKitConfig(paths: Pick<AgentKitPaths, "agentDir">, cliVersion: string, now?: () => Date): KitConfig;
26
+ export declare function writeKitConfig(paths: Pick<AgentKitPaths, "kitConfigPath">, config: KitConfig): void;
27
+ /** Read kit.json, falling back to defaults for any missing/invalid field. */
28
+ export declare function readKitConfig(paths: Pick<AgentKitPaths, "kitConfigPath" | "agentDir">, cliVersion: string): KitConfig;
29
+ //# sourceMappingURL=kit-config.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * ~/.hq-agent/kit.json — settings `hq agent kit install` records so the
3
+ * services (which only receive `hq agent kit run <service>`) know where to
4
+ * sync and how often. Kept separate from machine-creds.json, which is the
5
+ * contract-shaped credential file and nothing else.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ export const DEFAULT_SYNC_INTERVAL_MS = 5 * 60_000;
10
+ export const DEFAULT_INBOX_POLL_MS = 15_000;
11
+ export const DEFAULT_HEARTBEAT_INTERVAL_MS = 60_000;
12
+ export const DEFAULT_MESH_REFRESH_MS = 5 * 60_000;
13
+ export function defaultHqRoot(paths) {
14
+ return path.join(paths.agentDir, "hq");
15
+ }
16
+ export function defaultKitConfig(paths, cliVersion, now = () => new Date()) {
17
+ return {
18
+ hqRoot: defaultHqRoot(paths),
19
+ syncIntervalMs: DEFAULT_SYNC_INTERVAL_MS,
20
+ inboxPollMs: DEFAULT_INBOX_POLL_MS,
21
+ heartbeatIntervalMs: DEFAULT_HEARTBEAT_INTERVAL_MS,
22
+ meshRefreshMs: DEFAULT_MESH_REFRESH_MS,
23
+ inboxAck: false,
24
+ installedAt: now().toISOString(),
25
+ cliVersion,
26
+ };
27
+ }
28
+ export function writeKitConfig(paths, config) {
29
+ fs.mkdirSync(path.dirname(paths.kitConfigPath), { recursive: true, mode: 0o700 });
30
+ fs.writeFileSync(paths.kitConfigPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
31
+ }
32
+ /** Read kit.json, falling back to defaults for any missing/invalid field. */
33
+ export function readKitConfig(paths, cliVersion) {
34
+ const base = defaultKitConfig(paths, cliVersion);
35
+ let raw;
36
+ try {
37
+ raw = JSON.parse(fs.readFileSync(paths.kitConfigPath, "utf8"));
38
+ }
39
+ catch {
40
+ return base;
41
+ }
42
+ const num = (k) => typeof raw[k] === "number" && raw[k] > 0 ? raw[k] : base[k];
43
+ return {
44
+ hqRoot: typeof raw.hqRoot === "string" && raw.hqRoot ? raw.hqRoot : base.hqRoot,
45
+ syncIntervalMs: num("syncIntervalMs"),
46
+ inboxPollMs: num("inboxPollMs"),
47
+ heartbeatIntervalMs: num("heartbeatIntervalMs"),
48
+ meshRefreshMs: num("meshRefreshMs"),
49
+ inboxAck: raw.inboxAck === true,
50
+ installedAt: typeof raw.installedAt === "string" ? raw.installedAt : base.installedAt,
51
+ cliVersion: typeof raw.cliVersion === "string" ? raw.cliVersion : base.cliVersion,
52
+ };
53
+ }
54
+ //# sourceMappingURL=kit-config.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Per-service log under ~/.hq-agent/logs/<service>.log, rotated at 5 MB.
3
+ * Lines are sanitized so a bearer token or secret reference can never land
4
+ * on disk even through an error message.
5
+ */
6
+ import type { AgentKitPaths } from "./paths.js";
7
+ export declare const KIT_LOG_MAX_BYTES: number;
8
+ export type KitLogLevel = "info" | "warn" | "error";
9
+ export type KitLogger = (level: KitLogLevel, message: string) => void;
10
+ export declare function sanitizeLogLine(message: string): string;
11
+ export declare function formatKitLogLine(level: KitLogLevel, message: string, now?: () => Date): string;
12
+ export declare function createKitLogger(paths: Pick<AgentKitPaths, "logsDir">, service: string, opts?: {
13
+ now?: () => Date;
14
+ echo?: boolean;
15
+ maxBytes?: number;
16
+ }): KitLogger;
17
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Per-service log under ~/.hq-agent/logs/<service>.log, rotated at 5 MB.
3
+ * Lines are sanitized so a bearer token or secret reference can never land
4
+ * on disk even through an error message.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { serviceLogPath } from "./paths.js";
9
+ export const KIT_LOG_MAX_BYTES = 5 * 1024 * 1024;
10
+ export function sanitizeLogLine(message) {
11
+ return message
12
+ .replace(/Bearer\s+[A-Za-z0-9._-]+/gi, "Bearer [REDACTED]")
13
+ .replace(/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, "[JWT REDACTED]")
14
+ .replace(/"secret"\s*:\s*"[^"]*"/g, '"secret":"[REDACTED]"')
15
+ .replace(/\r?\n/g, " ")
16
+ .slice(0, 1000);
17
+ }
18
+ export function formatKitLogLine(level, message, now = () => new Date()) {
19
+ return `${now().toISOString()} ${level} ${sanitizeLogLine(message)}`;
20
+ }
21
+ export function createKitLogger(paths, service, opts = {}) {
22
+ const file = serviceLogPath(paths, service);
23
+ const maxBytes = opts.maxBytes ?? KIT_LOG_MAX_BYTES;
24
+ return (level, message) => {
25
+ const line = formatKitLogLine(level, message, opts.now);
26
+ try {
27
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
28
+ try {
29
+ if (fs.statSync(file).size > maxBytes) {
30
+ fs.renameSync(file, `${file}.1`);
31
+ }
32
+ }
33
+ catch {
34
+ /* no file yet */
35
+ }
36
+ fs.appendFileSync(file, `${line}\n`, { mode: 0o600 });
37
+ }
38
+ catch {
39
+ /* best-effort */
40
+ }
41
+ if (opts.echo) {
42
+ (level === "error" ? process.stderr : process.stdout).write(`${line}\n`);
43
+ }
44
+ };
45
+ }
46
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Minimal stdio MCP server core (JSON-RPC 2.0, newline-delimited).
3
+ *
4
+ * hq-cli carries no MCP SDK dependency and the full HQ MCP surface lives in
5
+ * the separate @indigoai-us/hq-mcp package (a bin, not a library). `hq agent
6
+ * mcp` needs only the tools subset of the protocol — initialize, ping,
7
+ * tools/list, tools/call — so this module implements exactly that over a
8
+ * transport-agnostic `dispatch()` (tests drive it directly) plus a stdio
9
+ * pump. Every request is answered; notifications are consumed silently;
10
+ * malformed input yields a JSON-RPC error rather than a crash.
11
+ */
12
+ export declare const MCP_PROTOCOL_VERSIONS: readonly ["2025-06-18", "2025-03-26", "2024-11-05"];
13
+ export declare const MCP_LATEST_PROTOCOL_VERSION: "2025-06-18";
14
+ export interface JsonRpcRequest {
15
+ jsonrpc: "2.0";
16
+ id?: string | number | null;
17
+ method: string;
18
+ params?: unknown;
19
+ }
20
+ export interface JsonRpcResponse {
21
+ jsonrpc: "2.0";
22
+ id: string | number | null;
23
+ result?: unknown;
24
+ error?: {
25
+ code: number;
26
+ message: string;
27
+ data?: unknown;
28
+ };
29
+ }
30
+ export declare const JSONRPC_PARSE_ERROR = -32700;
31
+ export declare const JSONRPC_INVALID_REQUEST = -32600;
32
+ export declare const JSONRPC_METHOD_NOT_FOUND = -32601;
33
+ export declare const JSONRPC_INVALID_PARAMS = -32602;
34
+ export declare const JSONRPC_INTERNAL_ERROR = -32603;
35
+ export interface McpToolDefinition {
36
+ name: string;
37
+ description: string;
38
+ inputSchema: Record<string, unknown>;
39
+ }
40
+ export interface McpToolContent {
41
+ type: "text";
42
+ text: string;
43
+ }
44
+ export interface McpToolResult {
45
+ content: McpToolContent[];
46
+ isError?: boolean;
47
+ }
48
+ export type McpToolHandler = (args: Record<string, unknown>) => Promise<McpToolResult>;
49
+ export interface McpTool extends McpToolDefinition {
50
+ handler: McpToolHandler;
51
+ }
52
+ export interface McpServerOptions {
53
+ name: string;
54
+ version: string;
55
+ instructions?: string;
56
+ tools: McpTool[];
57
+ }
58
+ export declare class McpToolInputError extends Error {
59
+ constructor(message: string);
60
+ }
61
+ export declare function textResult(text: string): McpToolResult;
62
+ export declare function errorResult(text: string): McpToolResult;
63
+ export declare class McpServer {
64
+ private readonly opts;
65
+ private readonly tools;
66
+ private initialized;
67
+ constructor(opts: McpServerOptions);
68
+ listTools(): McpToolDefinition[];
69
+ /**
70
+ * Handle one decoded message. Returns null for notifications (no reply)
71
+ * and for responses the client sends us (we issue no requests).
72
+ */
73
+ dispatch(message: unknown): Promise<JsonRpcResponse | null>;
74
+ /** Decode one line, dispatch, and encode the reply (or null). */
75
+ handleLine(line: string): Promise<string | null>;
76
+ isInitialized(): boolean;
77
+ /**
78
+ * Pump newline-delimited messages from `input` to `output` until EOF.
79
+ * Requests are processed strictly in order so tool side effects (a DM
80
+ * send, a secrets exec) never interleave.
81
+ */
82
+ serve(input: NodeJS.ReadableStream, output: NodeJS.WritableStream): Promise<void>;
83
+ }
84
+ //# sourceMappingURL=jsonrpc.d.ts.map
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Minimal stdio MCP server core (JSON-RPC 2.0, newline-delimited).
3
+ *
4
+ * hq-cli carries no MCP SDK dependency and the full HQ MCP surface lives in
5
+ * the separate @indigoai-us/hq-mcp package (a bin, not a library). `hq agent
6
+ * mcp` needs only the tools subset of the protocol — initialize, ping,
7
+ * tools/list, tools/call — so this module implements exactly that over a
8
+ * transport-agnostic `dispatch()` (tests drive it directly) plus a stdio
9
+ * pump. Every request is answered; notifications are consumed silently;
10
+ * malformed input yields a JSON-RPC error rather than a crash.
11
+ */
12
+ export const MCP_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
13
+ export const MCP_LATEST_PROTOCOL_VERSION = MCP_PROTOCOL_VERSIONS[0];
14
+ export const JSONRPC_PARSE_ERROR = -32700;
15
+ export const JSONRPC_INVALID_REQUEST = -32600;
16
+ export const JSONRPC_METHOD_NOT_FOUND = -32601;
17
+ export const JSONRPC_INVALID_PARAMS = -32602;
18
+ export const JSONRPC_INTERNAL_ERROR = -32603;
19
+ export class McpToolInputError extends Error {
20
+ constructor(message) {
21
+ super(message);
22
+ this.name = "McpToolInputError";
23
+ }
24
+ }
25
+ export function textResult(text) {
26
+ return { content: [{ type: "text", text }] };
27
+ }
28
+ export function errorResult(text) {
29
+ return { content: [{ type: "text", text }], isError: true };
30
+ }
31
+ function ok(id, result) {
32
+ return { jsonrpc: "2.0", id, result };
33
+ }
34
+ function fail(id, code, message, data) {
35
+ return { jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined ? { data } : {}) } };
36
+ }
37
+ export class McpServer {
38
+ opts;
39
+ tools = new Map();
40
+ initialized = false;
41
+ constructor(opts) {
42
+ this.opts = opts;
43
+ for (const tool of opts.tools) {
44
+ if (this.tools.has(tool.name))
45
+ throw new Error(`duplicate MCP tool ${tool.name}`);
46
+ this.tools.set(tool.name, tool);
47
+ }
48
+ }
49
+ listTools() {
50
+ return [...this.tools.values()].map(({ name, description, inputSchema }) => ({
51
+ name,
52
+ description,
53
+ inputSchema,
54
+ }));
55
+ }
56
+ /**
57
+ * Handle one decoded message. Returns null for notifications (no reply)
58
+ * and for responses the client sends us (we issue no requests).
59
+ */
60
+ async dispatch(message) {
61
+ if (!message || typeof message !== "object" || Array.isArray(message)) {
62
+ return fail(null, JSONRPC_INVALID_REQUEST, "expected a JSON-RPC object");
63
+ }
64
+ const msg = message;
65
+ if (typeof msg.method !== "string") {
66
+ // A response to a server-initiated request; we never send any.
67
+ if ("result" in msg || "error" in msg)
68
+ return null;
69
+ return fail(msg.id ?? null, JSONRPC_INVALID_REQUEST, "missing method");
70
+ }
71
+ const isNotification = msg.id === undefined;
72
+ const id = msg.id ?? null;
73
+ const params = (msg.params && typeof msg.params === "object" ? msg.params : {});
74
+ if (isNotification) {
75
+ if (msg.method === "notifications/initialized")
76
+ this.initialized = true;
77
+ return null;
78
+ }
79
+ switch (msg.method) {
80
+ case "initialize": {
81
+ const requested = typeof params.protocolVersion === "string" ? params.protocolVersion : "";
82
+ const protocolVersion = MCP_PROTOCOL_VERSIONS.includes(requested)
83
+ ? requested
84
+ : MCP_LATEST_PROTOCOL_VERSION;
85
+ return ok(id, {
86
+ protocolVersion,
87
+ capabilities: { tools: { listChanged: false } },
88
+ serverInfo: { name: this.opts.name, version: this.opts.version },
89
+ ...(this.opts.instructions ? { instructions: this.opts.instructions } : {}),
90
+ });
91
+ }
92
+ case "ping":
93
+ return ok(id, {});
94
+ case "tools/list":
95
+ return ok(id, { tools: this.listTools() });
96
+ case "tools/call": {
97
+ const name = typeof params.name === "string" ? params.name : "";
98
+ const tool = this.tools.get(name);
99
+ if (!tool)
100
+ return fail(id, JSONRPC_INVALID_PARAMS, `unknown tool: ${name || "(missing name)"}`);
101
+ const args = params.arguments && typeof params.arguments === "object" && !Array.isArray(params.arguments)
102
+ ? params.arguments
103
+ : {};
104
+ try {
105
+ return ok(id, await tool.handler(args));
106
+ }
107
+ catch (err) {
108
+ if (err instanceof McpToolInputError)
109
+ return fail(id, JSONRPC_INVALID_PARAMS, err.message);
110
+ // Tool execution failures are results, not protocol errors, so the
111
+ // model can read and react to them.
112
+ return ok(id, errorResult(err instanceof Error ? err.message : String(err)));
113
+ }
114
+ }
115
+ default:
116
+ return fail(id, JSONRPC_METHOD_NOT_FOUND, `method not found: ${msg.method}`);
117
+ }
118
+ }
119
+ /** Decode one line, dispatch, and encode the reply (or null). */
120
+ async handleLine(line) {
121
+ const trimmed = line.trim();
122
+ if (!trimmed)
123
+ return null;
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(trimmed);
127
+ }
128
+ catch {
129
+ return JSON.stringify(fail(null, JSONRPC_PARSE_ERROR, "invalid JSON"));
130
+ }
131
+ const reply = await this.dispatch(parsed);
132
+ return reply ? JSON.stringify(reply) : null;
133
+ }
134
+ isInitialized() {
135
+ return this.initialized;
136
+ }
137
+ /**
138
+ * Pump newline-delimited messages from `input` to `output` until EOF.
139
+ * Requests are processed strictly in order so tool side effects (a DM
140
+ * send, a secrets exec) never interleave.
141
+ */
142
+ async serve(input, output) {
143
+ let buffer = "";
144
+ const write = (s) => new Promise((resolve, reject) => output.write(`${s}\n`, (err) => (err ? reject(err) : resolve())));
145
+ input.setEncoding("utf8");
146
+ for await (const chunk of input) {
147
+ buffer += chunk;
148
+ let nl;
149
+ while ((nl = buffer.indexOf("\n")) >= 0) {
150
+ const line = buffer.slice(0, nl);
151
+ buffer = buffer.slice(nl + 1);
152
+ const reply = await this.handleLine(line);
153
+ if (reply)
154
+ await write(reply);
155
+ }
156
+ }
157
+ if (buffer.trim()) {
158
+ const reply = await this.handleLine(buffer);
159
+ if (reply)
160
+ await write(reply);
161
+ }
162
+ }
163
+ }
164
+ //# sourceMappingURL=jsonrpc.js.map