@indigoai-us/hq-cli 5.109.16 → 5.111.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +363 -0
- package/assets/bot-workers/setup/context/quick-reference.md +199 -0
- package/assets/bot-workers/setup/skills/first-company.md +71 -0
- package/assets/bot-workers/setup/skills/standing-help.md +74 -0
- package/assets/bot-workers/setup/worker.yaml +422 -0
- package/dist/commands/bot-continuity.d.ts +28 -0
- package/dist/commands/bot-continuity.js +68 -0
- package/dist/commands/bot.d.ts +73 -0
- package/dist/commands/bot.js +776 -0
- package/dist/commands/skill.d.ts +12 -3
- package/dist/commands/skill.js +79 -3
- package/dist/commands/workers.d.ts +2 -14
- package/dist/commands/workers.js +2 -8
- package/dist/lib/bot/api.d.ts +202 -0
- package/dist/lib/bot/api.js +202 -0
- package/dist/lib/bot/company-bind.d.ts +27 -0
- package/dist/lib/bot/company-bind.js +62 -0
- package/dist/lib/bot/config.d.ts +106 -0
- package/dist/lib/bot/config.js +141 -0
- package/dist/lib/bot/continuity-download.d.ts +28 -0
- package/dist/lib/bot/continuity-download.js +75 -0
- package/dist/lib/bot/continuity-install.d.ts +14 -0
- package/dist/lib/bot/continuity-install.js +101 -0
- package/dist/lib/bot/continuity.d.ts +66 -0
- package/dist/lib/bot/continuity.js +301 -0
- package/dist/lib/bot/creds.d.ts +24 -0
- package/dist/lib/bot/creds.js +51 -0
- package/dist/lib/bot/daemon.d.ts +75 -0
- package/dist/lib/bot/daemon.js +316 -0
- package/dist/lib/bot/inbox-state.d.ts +18 -0
- package/dist/lib/bot/inbox-state.js +51 -0
- package/dist/lib/bot/index.d.ts +16 -0
- package/dist/lib/bot/index.js +16 -0
- package/dist/lib/bot/inflight.d.ts +40 -0
- package/dist/lib/bot/inflight.js +44 -0
- package/dist/lib/bot/log.d.ts +13 -0
- package/dist/lib/bot/log.js +59 -0
- package/dist/lib/bot/owner-context.d.ts +75 -0
- package/dist/lib/bot/owner-context.js +151 -0
- package/dist/lib/bot/paths.d.ts +61 -0
- package/dist/lib/bot/paths.js +103 -0
- package/dist/lib/bot/progress.d.ts +84 -0
- package/dist/lib/bot/progress.js +167 -0
- package/dist/lib/bot/promote.d.ts +16 -0
- package/dist/lib/bot/promote.js +106 -0
- package/dist/lib/bot/promotion-hold.d.ts +24 -0
- package/dist/lib/bot/promotion-hold.js +103 -0
- package/dist/lib/bot/promotion-receipt.d.ts +9 -0
- package/dist/lib/bot/promotion-receipt.js +56 -0
- package/dist/lib/bot/promotion-upload.d.ts +16 -0
- package/dist/lib/bot/promotion-upload.js +65 -0
- package/dist/lib/bot/prompt.d.ts +103 -0
- package/dist/lib/bot/prompt.js +329 -0
- package/dist/lib/bot/room-policy.d.ts +53 -0
- package/dist/lib/bot/room-policy.js +73 -0
- package/dist/lib/bot/run.d.ts +98 -0
- package/dist/lib/bot/run.js +787 -0
- package/dist/lib/bot/runtime/claude.d.ts +49 -0
- package/dist/lib/bot/runtime/claude.js +151 -0
- package/dist/lib/bot/runtime/codex.d.ts +28 -0
- package/dist/lib/bot/runtime/codex.js +147 -0
- package/dist/lib/bot/runtime/grok.d.ts +16 -0
- package/dist/lib/bot/runtime/grok.js +67 -0
- package/dist/lib/bot/runtime/index.d.ts +35 -0
- package/dist/lib/bot/runtime/index.js +279 -0
- package/dist/lib/bot/runtime/messages-stream.d.ts +27 -0
- package/dist/lib/bot/runtime/messages-stream.js +85 -0
- package/dist/lib/bot/runtime/types.d.ts +136 -0
- package/dist/lib/bot/runtime/types.js +51 -0
- package/dist/lib/bot/scaffold.d.ts +38 -0
- package/dist/lib/bot/scaffold.js +94 -0
- package/dist/lib/bot/session.d.ts +19 -0
- package/dist/lib/bot/session.js +39 -0
- package/dist/lib/bot/status.d.ts +40 -0
- package/dist/lib/bot/status.js +66 -0
- package/dist/lib/bot/worker-source.d.ts +66 -0
- package/dist/lib/bot/worker-source.js +283 -0
- package/dist/lib/workers-registry/read.d.ts +15 -0
- package/dist/lib/workers-registry/read.js +17 -0
- package/dist/register-all.js +2 -0
- package/package.json +2 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* personal/workers/<name>/ scaffold for a local bot (local-bots US-005/US-006).
|
|
3
|
+
* Never overwrites an existing file (the bot edits its own memory).
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { botWorkerRelDir } from "./paths.js";
|
|
8
|
+
export function renderWorkerYaml(name, agentUid, runtime, memoryDir) {
|
|
9
|
+
const memory = (memoryDir?.trim() || `personal/workers/${name}/memory`).replace(/\/+$/, "");
|
|
10
|
+
return `worker:
|
|
11
|
+
id: ${name}
|
|
12
|
+
name: "${name}"
|
|
13
|
+
description: "Personal local HQ bot (${runtime}) — runs on this computer with the owner's permissions"
|
|
14
|
+
type: PersonalBot
|
|
15
|
+
version: "1.0"
|
|
16
|
+
kind: local-bot
|
|
17
|
+
agent_uid: ${agentUid}
|
|
18
|
+
|
|
19
|
+
execution:
|
|
20
|
+
mode: resident
|
|
21
|
+
runtime: ${runtime}
|
|
22
|
+
managed_by: hq bot
|
|
23
|
+
|
|
24
|
+
context:
|
|
25
|
+
base:
|
|
26
|
+
- personal/workers/${name}/persona.md
|
|
27
|
+
- ${memory}/
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
export function renderPersonaMd(name) {
|
|
31
|
+
return `# ${name}
|
|
32
|
+
|
|
33
|
+
You are ${name}, a personal assistant bot that lives in this HQ.
|
|
34
|
+
|
|
35
|
+
## Voice
|
|
36
|
+
|
|
37
|
+
- Warm, direct, and brief. Answer the person, not the mechanics.
|
|
38
|
+
- Ask one question at a time when something is genuinely ambiguous.
|
|
39
|
+
- Never paste secrets, tokens, or credentials into a reply.
|
|
40
|
+
|
|
41
|
+
## Standing instructions
|
|
42
|
+
|
|
43
|
+
- Prefer HQ's own skills and \`hq\` commands over hand-rolled scripts.
|
|
44
|
+
- When you finish a task, say what changed and where in one or two lines.
|
|
45
|
+
- Keep durable facts about your owner in \`memory/\` (short markdown notes).
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
export function renderMemoryReadme(name) {
|
|
49
|
+
return `# ${name} memory
|
|
50
|
+
|
|
51
|
+
Short markdown notes ${name} keeps between conversations: preferences, recurring
|
|
52
|
+
context, and things the owner asked it to remember. ${name} may edit these files;
|
|
53
|
+
you can too. Never store secrets here.
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
56
|
+
function writeIfMissing(io, file, body, result) {
|
|
57
|
+
if (io.existsSync(file)) {
|
|
58
|
+
result.kept.push(file);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
io.writeFileSync(file, body, { mode: 0o644 });
|
|
62
|
+
result.created.push(file);
|
|
63
|
+
}
|
|
64
|
+
/** Absolute memory folder for a scaffold: the override (relative to hqRoot unless absolute) or the default. */
|
|
65
|
+
export function resolveScaffoldMemoryDir(hqRoot, name, memoryDir) {
|
|
66
|
+
const raw = memoryDir?.trim();
|
|
67
|
+
if (!raw)
|
|
68
|
+
return path.join(hqRoot, botWorkerRelDir(name), "memory");
|
|
69
|
+
return path.isAbsolute(raw) ? raw : path.join(hqRoot, raw);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The memory folder + README only — the memory a bot keeps between
|
|
73
|
+
* conversations: personal/workers/<name>/memory/ by default, or the given
|
|
74
|
+
* `memoryDir` (absolute for local memory). Used on its own for worker-sourced
|
|
75
|
+
* bots, whose definition lives in the company tree and must not be written
|
|
76
|
+
* to. Never overwrites an existing file.
|
|
77
|
+
*/
|
|
78
|
+
export function scaffoldBotMemory(hqRoot, name, io = fs, opts = {}) {
|
|
79
|
+
const dir = path.join(hqRoot, botWorkerRelDir(name));
|
|
80
|
+
const memoryDir = resolveScaffoldMemoryDir(hqRoot, name, opts.memoryDir);
|
|
81
|
+
const result = { dir, memoryDir, created: [], kept: [] };
|
|
82
|
+
io.mkdirSync(memoryDir, { recursive: true });
|
|
83
|
+
writeIfMissing(io, path.join(memoryDir, "README.md"), renderMemoryReadme(name), result);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
export function scaffoldBotWorker(hqRoot, name, agentUid, runtime, io = fs, opts = {}) {
|
|
87
|
+
const result = scaffoldBotMemory(hqRoot, name, io, opts);
|
|
88
|
+
const { dir } = result;
|
|
89
|
+
io.mkdirSync(dir, { recursive: true });
|
|
90
|
+
writeIfMissing(io, path.join(dir, "worker.yaml"), renderWorkerYaml(name, agentUid, runtime, opts.memoryDir), result);
|
|
91
|
+
writeIfMissing(io, path.join(dir, "persona.md"), renderPersonaMd(name), result);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session.json — the model CLI's own session id so each DM turn resumes the
|
|
3
|
+
* same conversation (one headless invocation per DM, resumed by id).
|
|
4
|
+
* Rooms get their own file per channel (session-room-<channelId>.json) so the
|
|
5
|
+
* owner's DM context is never carried into a group chat, and vice versa.
|
|
6
|
+
*/
|
|
7
|
+
import { type BotSessionScope } from "./paths.js";
|
|
8
|
+
import type { BotRuntimeId } from "./config.js";
|
|
9
|
+
export interface BotSessionFile {
|
|
10
|
+
v: 1;
|
|
11
|
+
runtime: BotRuntimeId;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
turns: number;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function readBotSession(dir: string, runtime: BotRuntimeId, scope?: BotSessionScope): BotSessionFile | null;
|
|
17
|
+
export declare function writeBotSession(dir: string, session: BotSessionFile, scope?: BotSessionScope): void;
|
|
18
|
+
export declare function clearBotSession(dir: string, scope?: BotSessionScope): void;
|
|
19
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session.json — the model CLI's own session id so each DM turn resumes the
|
|
3
|
+
* same conversation (one headless invocation per DM, resumed by id).
|
|
4
|
+
* Rooms get their own file per channel (session-room-<channelId>.json) so the
|
|
5
|
+
* owner's DM context is never carried into a group chat, and vice versa.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { botSessionPath } from "./paths.js";
|
|
10
|
+
export function readBotSession(dir, runtime, scope = "dm") {
|
|
11
|
+
const p = botSessionPath(dir, scope);
|
|
12
|
+
if (!fs.existsSync(p))
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
16
|
+
if (raw.v !== 1 || raw.runtime !== runtime || typeof raw.sessionId !== "string")
|
|
17
|
+
return null;
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function writeBotSession(dir, session, scope = "dm") {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
26
|
+
const dest = botSessionPath(dir, scope);
|
|
27
|
+
const tmp = path.join(dir, `.session.tmp.${process.pid}.${Date.now()}`);
|
|
28
|
+
fs.writeFileSync(tmp, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
|
|
29
|
+
fs.renameSync(tmp, dest);
|
|
30
|
+
}
|
|
31
|
+
export function clearBotSession(dir, scope = "dm") {
|
|
32
|
+
try {
|
|
33
|
+
fs.unlinkSync(botSessionPath(dir, scope));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* already gone */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status.json — atomic, metadata-only runtime status (local-bots US-003).
|
|
3
|
+
*/
|
|
4
|
+
import type { BotRuntimeId } from "./config.js";
|
|
5
|
+
export type BotState = "starting" | "running" | "stopped" | "failed";
|
|
6
|
+
export interface BotStatusFile {
|
|
7
|
+
v: 1;
|
|
8
|
+
pid: number;
|
|
9
|
+
state: BotState;
|
|
10
|
+
runtime: BotRuntimeId;
|
|
11
|
+
startedAt: string;
|
|
12
|
+
lastActivityAt: string;
|
|
13
|
+
/** Model-CLI restarts since this process started. */
|
|
14
|
+
restarts: number;
|
|
15
|
+
/** Consecutive model-CLI failures inside the failure window. */
|
|
16
|
+
recentFailures: number;
|
|
17
|
+
lastHeartbeatAt?: string;
|
|
18
|
+
lastHeartbeatOk?: boolean;
|
|
19
|
+
lastInboxPollAt?: string;
|
|
20
|
+
/** Bounded, redacted stderr excerpt of the last model-CLI failure. */
|
|
21
|
+
lastError?: string;
|
|
22
|
+
/** Inbound DMs answered since start. */
|
|
23
|
+
repliesSent: number;
|
|
24
|
+
/** True only after this process has installed its graceful promotion signal. */
|
|
25
|
+
canDrainForPromotion?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Claude only: permission mode the bot settled on after a fallback
|
|
28
|
+
* (e.g. "auto" when this Claude Code's settings disable bypassPermissions).
|
|
29
|
+
* Persisted so later turns skip the failed attempt.
|
|
30
|
+
*/
|
|
31
|
+
claudePermissionMode?: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function defaultBotStatus(pid: number, runtime: BotRuntimeId, now?: () => Date): BotStatusFile;
|
|
35
|
+
export declare function readBotStatus(dir: string): BotStatusFile | null;
|
|
36
|
+
export declare function writeBotStatus(dir: string, status: BotStatusFile): void;
|
|
37
|
+
export declare function patchBotStatus(dir: string, patch: Partial<BotStatusFile>, fallback: () => BotStatusFile, now?: () => Date): BotStatusFile;
|
|
38
|
+
/** Live process check for a status file's pid. */
|
|
39
|
+
export declare function isPidAlive(pid: number): boolean;
|
|
40
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status.json — atomic, metadata-only runtime status (local-bots US-003).
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { botStatusPath } from "./paths.js";
|
|
7
|
+
export function defaultBotStatus(pid, runtime, now = () => new Date()) {
|
|
8
|
+
const at = now().toISOString();
|
|
9
|
+
return {
|
|
10
|
+
v: 1,
|
|
11
|
+
pid,
|
|
12
|
+
state: "starting",
|
|
13
|
+
runtime,
|
|
14
|
+
startedAt: at,
|
|
15
|
+
lastActivityAt: at,
|
|
16
|
+
restarts: 0,
|
|
17
|
+
recentFailures: 0,
|
|
18
|
+
repliesSent: 0,
|
|
19
|
+
updatedAt: at,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function readBotStatus(dir) {
|
|
23
|
+
const p = botStatusPath(dir);
|
|
24
|
+
if (!fs.existsSync(p))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
28
|
+
if (raw.v !== 1 || typeof raw.pid !== "number")
|
|
29
|
+
return null;
|
|
30
|
+
return raw;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function writeBotStatus(dir, status) {
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
38
|
+
const dest = botStatusPath(dir);
|
|
39
|
+
const tmp = path.join(dir, `.status.tmp.${process.pid}.${Date.now()}`);
|
|
40
|
+
fs.writeFileSync(tmp, `${JSON.stringify(status, null, 2)}\n`, { mode: 0o600 });
|
|
41
|
+
fs.renameSync(tmp, dest);
|
|
42
|
+
}
|
|
43
|
+
export function patchBotStatus(dir, patch, fallback, now = () => new Date()) {
|
|
44
|
+
const current = readBotStatus(dir) ?? fallback();
|
|
45
|
+
const next = {
|
|
46
|
+
...current,
|
|
47
|
+
...patch,
|
|
48
|
+
v: 1,
|
|
49
|
+
updatedAt: now().toISOString(),
|
|
50
|
+
};
|
|
51
|
+
writeBotStatus(dir, next);
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
/** Live process check for a status file's pid. */
|
|
55
|
+
export function isPidAlive(pid) {
|
|
56
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
57
|
+
return false;
|
|
58
|
+
try {
|
|
59
|
+
process.kill(pid, 0);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workers a local bot can be created from (local-bots Feature 4; the desktop
|
|
3
|
+
* shows them as "templates").
|
|
4
|
+
*
|
|
5
|
+
* The registry (core/workers/registry.yaml) is the index; a worker is only
|
|
6
|
+
* offered when its worker.yaml actually exists on disk in the owner's synced
|
|
7
|
+
* tree — that tree is the ACL, so no session or company lookup is needed.
|
|
8
|
+
* Company workers (companies/<co>/workers/<id>) and core workers
|
|
9
|
+
* (core/workers/…) are both offered; personal bots' own scaffolds
|
|
10
|
+
* (personal/workers/*) are excluded. Because the registry generator keeps one
|
|
11
|
+
* entry per id, core workers it dropped are picked up straight from disk
|
|
12
|
+
* (core/workers/**\/worker.yaml, a few levels deep). Ids are unique across the
|
|
13
|
+
* registry; if a duplicate ever appears, a company worker (one with a company
|
|
14
|
+
* slug) wins over the core one, which in turn wins over an unslugged copy
|
|
15
|
+
* elsewhere in a company folder (e.g. a repo checkout's template workers).
|
|
16
|
+
* The CLI ships a setup-only fallback for a fresh HQ with no setup template.
|
|
17
|
+
*/
|
|
18
|
+
import { type RegistryWorker } from "../workers-registry/read.js";
|
|
19
|
+
export declare const WORKER_SUMMARY_MAX_CHARS = 140;
|
|
20
|
+
export type BotWorkerSource = "core" | "company";
|
|
21
|
+
export interface BotWorkerOption {
|
|
22
|
+
id: string;
|
|
23
|
+
/** Display name (`worker.name` in worker.yaml), when the worker has one. */
|
|
24
|
+
name?: string;
|
|
25
|
+
/** hqRoot-relative worker folder, no trailing slash. */
|
|
26
|
+
path: string;
|
|
27
|
+
type: string;
|
|
28
|
+
/** "core" for core/workers/…, "company" for everything else. */
|
|
29
|
+
source: BotWorkerSource;
|
|
30
|
+
/** Company slug; null for core workers (and registry entries without one). */
|
|
31
|
+
company: string | null;
|
|
32
|
+
status?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
/**
|
|
35
|
+
* One-line blurb for pickers: worker.yaml's top-level `summary:`, else the
|
|
36
|
+
* first sentence of the description, at most WORKER_SUMMARY_MAX_CHARS.
|
|
37
|
+
* Empty string when the worker has neither.
|
|
38
|
+
*/
|
|
39
|
+
summary: string;
|
|
40
|
+
/** Number of entries under `skills:` in worker.yaml. */
|
|
41
|
+
skillCount: number;
|
|
42
|
+
}
|
|
43
|
+
export interface WorkerSourceFs {
|
|
44
|
+
existsSync: (p: string) => boolean;
|
|
45
|
+
readFileSync?: (p: string, encoding: "utf8") => string;
|
|
46
|
+
readdirSync?: (p: string, opts: {
|
|
47
|
+
withFileTypes: true;
|
|
48
|
+
}) => Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
isDirectory: () => boolean;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
/** Collapse whitespace and take the first sentence, capped at `max` chars. */
|
|
54
|
+
export declare function summarizeDescription(text: string | undefined, max?: number): string;
|
|
55
|
+
/**
|
|
56
|
+
* Development-only override: extra folders whose immediate children hold a
|
|
57
|
+
* worker.yaml are offered as templates too, so a worker staged in a worktree
|
|
58
|
+
* can be tested before it is promoted into `core/`. Colon-separated absolute
|
|
59
|
+
* paths; unset means no change at all. Entries win over a same-id worker found
|
|
60
|
+
* anywhere else, which is the whole point — the staged copy is the one under
|
|
61
|
+
* test.
|
|
62
|
+
*/
|
|
63
|
+
export declare const EXTRA_WORKERS_DIR_ENV = "HQ_BOT_EXTRA_WORKERS_DIR";
|
|
64
|
+
export declare function listBotWorkerOptions(hqRoot: string, io?: WorkerSourceFs, registry?: RegistryWorker[], env?: NodeJS.ProcessEnv): BotWorkerOption[];
|
|
65
|
+
export declare function resolveBotWorker(hqRoot: string, workerId: string, io?: WorkerSourceFs, registry?: RegistryWorker[], env?: NodeJS.ProcessEnv): BotWorkerOption;
|
|
66
|
+
//# sourceMappingURL=worker-source.d.ts.map
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workers a local bot can be created from (local-bots Feature 4; the desktop
|
|
3
|
+
* shows them as "templates").
|
|
4
|
+
*
|
|
5
|
+
* The registry (core/workers/registry.yaml) is the index; a worker is only
|
|
6
|
+
* offered when its worker.yaml actually exists on disk in the owner's synced
|
|
7
|
+
* tree — that tree is the ACL, so no session or company lookup is needed.
|
|
8
|
+
* Company workers (companies/<co>/workers/<id>) and core workers
|
|
9
|
+
* (core/workers/…) are both offered; personal bots' own scaffolds
|
|
10
|
+
* (personal/workers/*) are excluded. Because the registry generator keeps one
|
|
11
|
+
* entry per id, core workers it dropped are picked up straight from disk
|
|
12
|
+
* (core/workers/**\/worker.yaml, a few levels deep). Ids are unique across the
|
|
13
|
+
* registry; if a duplicate ever appears, a company worker (one with a company
|
|
14
|
+
* slug) wins over the core one, which in turn wins over an unslugged copy
|
|
15
|
+
* elsewhere in a company folder (e.g. a repo checkout's template workers).
|
|
16
|
+
* The CLI ships a setup-only fallback for a fresh HQ with no setup template.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import * as yaml from "js-yaml";
|
|
21
|
+
import { packageRoot } from "../../utils/hq-roots.js";
|
|
22
|
+
import { readWorkerRegistry } from "../workers-registry/read.js";
|
|
23
|
+
export const WORKER_SUMMARY_MAX_CHARS = 140;
|
|
24
|
+
const CORE_WORKERS_REL = path.join("core", "workers");
|
|
25
|
+
/** core/workers/public/<team>/<id>/worker.yaml is the deepest layout in use. */
|
|
26
|
+
const CORE_SCAN_MAX_DEPTH = 4;
|
|
27
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", ".git", "skills", "memory"]);
|
|
28
|
+
function normalizeWorkerPath(p) {
|
|
29
|
+
return p.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
30
|
+
}
|
|
31
|
+
function workerSourceFor(rel) {
|
|
32
|
+
return rel === "core/workers" || rel.startsWith("core/workers/") ? "core" : "company";
|
|
33
|
+
}
|
|
34
|
+
/** Higher wins when two registry entries share an id. */
|
|
35
|
+
function duplicateRank(w, rel) {
|
|
36
|
+
if (workerSourceFor(rel) === "core")
|
|
37
|
+
return 1;
|
|
38
|
+
return w.company ? 2 : 0;
|
|
39
|
+
}
|
|
40
|
+
function asString(v) {
|
|
41
|
+
return typeof v === "string" && v.trim() ? v.trim() : undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Collapse whitespace and take the first sentence, capped at `max` chars. */
|
|
44
|
+
export function summarizeDescription(text, max = WORKER_SUMMARY_MAX_CHARS) {
|
|
45
|
+
const flat = (text ?? "").replace(/\s+/g, " ").trim();
|
|
46
|
+
if (!flat)
|
|
47
|
+
return "";
|
|
48
|
+
const m = /^(.*?[.!?])(?:\s|$)/.exec(flat);
|
|
49
|
+
let sentence = (m ? m[1] : flat).trim();
|
|
50
|
+
if (sentence.length > max) {
|
|
51
|
+
const cut = sentence.slice(0, max - 1);
|
|
52
|
+
const atWord = cut.lastIndexOf(" ");
|
|
53
|
+
sentence = `${(atWord > max / 2 ? cut.slice(0, atWord) : cut).replace(/[\s,;:]+$/, "")}…`;
|
|
54
|
+
}
|
|
55
|
+
return sentence;
|
|
56
|
+
}
|
|
57
|
+
function readWorkerYamlFacts(file, io) {
|
|
58
|
+
const out = { skillCount: 0 };
|
|
59
|
+
const read = io.readFileSync ?? ((p, enc) => fs.readFileSync(p, enc));
|
|
60
|
+
let doc = {};
|
|
61
|
+
try {
|
|
62
|
+
const parsed = yaml.load(read(file, "utf8"));
|
|
63
|
+
if (parsed && typeof parsed === "object")
|
|
64
|
+
doc = parsed;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
const meta = (doc.worker && typeof doc.worker === "object" ? doc.worker : {});
|
|
70
|
+
out.id = asString(meta.id);
|
|
71
|
+
out.name = asString(meta.name);
|
|
72
|
+
out.type = asString(meta.type);
|
|
73
|
+
out.status = asString(meta.status);
|
|
74
|
+
out.summary = asString(doc.summary);
|
|
75
|
+
out.description = asString(meta.description) ?? asString(doc.description);
|
|
76
|
+
out.skillCount = Array.isArray(doc.skills) ? doc.skills.length : 0;
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
function toOption(w, rel, facts) {
|
|
80
|
+
const description = w.description ?? facts.description;
|
|
81
|
+
const source = workerSourceFor(rel);
|
|
82
|
+
return {
|
|
83
|
+
id: w.id,
|
|
84
|
+
...(facts.name ? { name: facts.name } : {}),
|
|
85
|
+
path: rel,
|
|
86
|
+
type: w.type,
|
|
87
|
+
source,
|
|
88
|
+
company: source === "core" ? null : (w.company ?? null),
|
|
89
|
+
...(w.status ? { status: w.status } : {}),
|
|
90
|
+
...(description ? { description } : {}),
|
|
91
|
+
summary: facts.summary ? summarizeDescription(facts.summary) : summarizeDescription(description),
|
|
92
|
+
skillCount: facts.skillCount,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** hqRoot-relative folders under core/workers/ that hold a worker.yaml. */
|
|
96
|
+
function scanCoreWorkerDirs(hqRoot, io) {
|
|
97
|
+
const readdir = io.readdirSync ?? ((p, opts) => fs.readdirSync(p, opts));
|
|
98
|
+
const found = [];
|
|
99
|
+
const walk = (rel, depth) => {
|
|
100
|
+
if (depth > CORE_SCAN_MAX_DEPTH)
|
|
101
|
+
return;
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = readdir(path.join(hqRoot, rel), { withFileTypes: true });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (entries.some((e) => e.name === "worker.yaml" && !e.isDirectory())) {
|
|
110
|
+
found.push(rel);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const e of entries) {
|
|
114
|
+
if (!e.isDirectory() || e.name.startsWith(".") || SKIP_DIRS.has(e.name))
|
|
115
|
+
continue;
|
|
116
|
+
walk(path.join(rel, e.name), depth + 1);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
if (io.existsSync(path.join(hqRoot, CORE_WORKERS_REL)))
|
|
120
|
+
walk(CORE_WORKERS_REL, 0);
|
|
121
|
+
return found;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Development-only override: extra folders whose immediate children hold a
|
|
125
|
+
* worker.yaml are offered as templates too, so a worker staged in a worktree
|
|
126
|
+
* can be tested before it is promoted into `core/`. Colon-separated absolute
|
|
127
|
+
* paths; unset means no change at all. Entries win over a same-id worker found
|
|
128
|
+
* anywhere else, which is the whole point — the staged copy is the one under
|
|
129
|
+
* test.
|
|
130
|
+
*/
|
|
131
|
+
export const EXTRA_WORKERS_DIR_ENV = "HQ_BOT_EXTRA_WORKERS_DIR";
|
|
132
|
+
/** Above company (2) and core (1): a staged template outranks what is on disk. */
|
|
133
|
+
const EXTRA_RANK = 3;
|
|
134
|
+
/** Absolute, existing paths from the env var, in order, without duplicates. */
|
|
135
|
+
function extraWorkerRoots(env, io) {
|
|
136
|
+
const raw = env[EXTRA_WORKERS_DIR_ENV];
|
|
137
|
+
if (typeof raw !== "string" || !raw.trim())
|
|
138
|
+
return [];
|
|
139
|
+
const roots = [];
|
|
140
|
+
for (const part of raw.split(":")) {
|
|
141
|
+
const dir = part.trim();
|
|
142
|
+
// A relative or missing path is a misconfiguration, not an error: a stale
|
|
143
|
+
// export in a shell profile must never break `hq bot workers`.
|
|
144
|
+
if (!dir || !path.isAbsolute(dir))
|
|
145
|
+
continue;
|
|
146
|
+
if (!io.existsSync(dir) || roots.includes(dir))
|
|
147
|
+
continue;
|
|
148
|
+
roots.push(dir);
|
|
149
|
+
}
|
|
150
|
+
return roots;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Absolute worker folders directly under `root` (one level, unlike the core
|
|
154
|
+
* scan). A root that is a file, or unreadable, yields nothing: readdir throws
|
|
155
|
+
* ENOTDIR / EACCES and we swallow it.
|
|
156
|
+
*/
|
|
157
|
+
function scanExtraWorkerDirs(root, io) {
|
|
158
|
+
const readdir = io.readdirSync ?? ((p, opts) => fs.readdirSync(p, opts));
|
|
159
|
+
let entries;
|
|
160
|
+
try {
|
|
161
|
+
entries = readdir(root, { withFileTypes: true });
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const found = [];
|
|
167
|
+
for (const e of entries) {
|
|
168
|
+
if (!e.isDirectory() || e.name.startsWith(".") || SKIP_DIRS.has(e.name))
|
|
169
|
+
continue;
|
|
170
|
+
const dir = path.join(root, e.name);
|
|
171
|
+
if (io.existsSync(path.join(dir, "worker.yaml")))
|
|
172
|
+
found.push(dir);
|
|
173
|
+
}
|
|
174
|
+
return found;
|
|
175
|
+
}
|
|
176
|
+
export function listBotWorkerOptions(hqRoot, io = fs, registry = readWorkerRegistry(hqRoot), env = process.env) {
|
|
177
|
+
const byId = new Map();
|
|
178
|
+
// Duplicate id: company (slugged) > core > unslugged; ties keep the first.
|
|
179
|
+
const offer = (rank, option) => {
|
|
180
|
+
const existing = byId.get(option.id);
|
|
181
|
+
if (existing && existing.rank >= rank)
|
|
182
|
+
return;
|
|
183
|
+
byId.set(option.id, { rank, option });
|
|
184
|
+
};
|
|
185
|
+
const seenPaths = new Set();
|
|
186
|
+
for (const w of registry) {
|
|
187
|
+
if (!w?.id || !w.path)
|
|
188
|
+
continue;
|
|
189
|
+
const rel = normalizeWorkerPath(w.path);
|
|
190
|
+
if (rel.startsWith("personal/workers/") || rel === "personal/workers")
|
|
191
|
+
continue;
|
|
192
|
+
const yamlPath = path.join(hqRoot, rel, "worker.yaml");
|
|
193
|
+
if (!io.existsSync(yamlPath))
|
|
194
|
+
continue;
|
|
195
|
+
seenPaths.add(rel);
|
|
196
|
+
offer(duplicateRank(w, rel), toOption(w, rel, readWorkerYamlFacts(yamlPath, io)));
|
|
197
|
+
}
|
|
198
|
+
// Core workers the registry left out (its generator keeps one entry per id).
|
|
199
|
+
for (const rel of scanCoreWorkerDirs(hqRoot, io)) {
|
|
200
|
+
if (seenPaths.has(rel))
|
|
201
|
+
continue;
|
|
202
|
+
const facts = readWorkerYamlFacts(path.join(hqRoot, rel, "worker.yaml"), io);
|
|
203
|
+
const id = facts.id ?? path.basename(rel);
|
|
204
|
+
const entry = {
|
|
205
|
+
id,
|
|
206
|
+
path: rel,
|
|
207
|
+
type: facts.type ?? "Worker",
|
|
208
|
+
visibility: "public",
|
|
209
|
+
...(facts.status ? { status: facts.status } : {}),
|
|
210
|
+
...(facts.description ? { description: facts.description } : {}),
|
|
211
|
+
};
|
|
212
|
+
offer(duplicateRank(entry, rel), toOption(entry, rel, facts));
|
|
213
|
+
}
|
|
214
|
+
// Opt-in override, last so it outranks everything found above.
|
|
215
|
+
for (const root of extraWorkerRoots(env, io)) {
|
|
216
|
+
for (const abs of scanExtraWorkerDirs(root, io)) {
|
|
217
|
+
const facts = readWorkerYamlFacts(path.join(abs, "worker.yaml"), io);
|
|
218
|
+
const id = facts.id ?? path.basename(abs);
|
|
219
|
+
// Stay hqRoot-relative (possibly via `..`): callers resolve the folder as
|
|
220
|
+
// path.join(hqRoot, option.path), which an absolute path would break.
|
|
221
|
+
const rel = normalizeWorkerPath(path.relative(hqRoot, abs));
|
|
222
|
+
const entry = {
|
|
223
|
+
id,
|
|
224
|
+
path: rel,
|
|
225
|
+
type: facts.type ?? "Worker",
|
|
226
|
+
visibility: "public",
|
|
227
|
+
...(facts.status ? { status: facts.status } : {}),
|
|
228
|
+
...(facts.description ? { description: facts.description } : {}),
|
|
229
|
+
};
|
|
230
|
+
// Presented as a core worker: it is a core template being staged.
|
|
231
|
+
offer(EXTRA_RANK, { ...toOption(entry, rel, facts), source: "core", company: null });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Setup must work before an HQ has synced core workers. Never replace a
|
|
235
|
+
// workspace or explicitly staged template, and never discover other bundled ids.
|
|
236
|
+
if (!byId.has("setup")) {
|
|
237
|
+
const dir = path.join(packageRoot(), "assets", "bot-workers", "setup");
|
|
238
|
+
const yamlPath = path.join(dir, "worker.yaml");
|
|
239
|
+
if (io.existsSync(yamlPath)) {
|
|
240
|
+
const facts = readWorkerYamlFacts(yamlPath, io);
|
|
241
|
+
if (facts.id === "setup") {
|
|
242
|
+
const rel = normalizeWorkerPath(path.relative(hqRoot, dir));
|
|
243
|
+
const entry = { id: "setup", path: rel, type: facts.type ?? "Worker", visibility: "public" };
|
|
244
|
+
offer(-1, { ...toOption(entry, rel, facts), source: "core", company: null });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return [...byId.values()].map((e) => e.option).sort((a, b) => a.id.localeCompare(b.id));
|
|
249
|
+
}
|
|
250
|
+
function nearestIds(target, ids, max = 5) {
|
|
251
|
+
const t = target.toLowerCase();
|
|
252
|
+
const scored = ids.map((id) => {
|
|
253
|
+
const l = id.toLowerCase();
|
|
254
|
+
let score = 3;
|
|
255
|
+
if (l.includes(t) || t.includes(l))
|
|
256
|
+
score = 0;
|
|
257
|
+
else if (l.startsWith(t.slice(0, 3)))
|
|
258
|
+
score = 1;
|
|
259
|
+
else if (t.split(/[-_]/).some((part) => part.length > 2 && l.includes(part)))
|
|
260
|
+
score = 2;
|
|
261
|
+
return { id, score };
|
|
262
|
+
});
|
|
263
|
+
return scored
|
|
264
|
+
.sort((a, b) => a.score - b.score || a.id.localeCompare(b.id))
|
|
265
|
+
.filter((s) => s.score < 3)
|
|
266
|
+
.slice(0, max)
|
|
267
|
+
.map((s) => s.id);
|
|
268
|
+
}
|
|
269
|
+
export function resolveBotWorker(hqRoot, workerId, io = fs, registry, env = process.env) {
|
|
270
|
+
const id = (workerId ?? "").trim();
|
|
271
|
+
const options = listBotWorkerOptions(hqRoot, io, registry, env);
|
|
272
|
+
const hit = options.find((o) => o.id === id);
|
|
273
|
+
if (hit)
|
|
274
|
+
return hit;
|
|
275
|
+
const near = nearestIds(id, options.map((o) => o.id));
|
|
276
|
+
const hint = near.length > 0
|
|
277
|
+
? ` Did you mean: ${near.join(", ")}?`
|
|
278
|
+
: options.length > 0
|
|
279
|
+
? ` Run \`hq bot workers\` to see the ${options.length} available.`
|
|
280
|
+
: " No workers are available in this HQ.";
|
|
281
|
+
throw Object.assign(new Error(`No worker "${id}" in this HQ.${hint}`), { expected: true });
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=worker-source.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface RegistryWorker {
|
|
2
|
+
id: string;
|
|
3
|
+
path: string;
|
|
4
|
+
type: string;
|
|
5
|
+
visibility: string;
|
|
6
|
+
company?: string;
|
|
7
|
+
team?: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
triggers?: string;
|
|
11
|
+
grants?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Read + parse the worker registry. Empty array if it does not exist. */
|
|
14
|
+
export declare function readWorkerRegistry(hqRoot: string): RegistryWorker[];
|
|
15
|
+
//# sourceMappingURL=read.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for the auto-generated core/workers/registry.yaml (see ./index.ts
|
|
3
|
+
* for the generator). Lives in lib so bot code can use it without importing
|
|
4
|
+
* a command module.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
9
|
+
/** Read + parse the worker registry. Empty array if it does not exist. */
|
|
10
|
+
export function readWorkerRegistry(hqRoot) {
|
|
11
|
+
const p = path.join(hqRoot, "core/workers/registry.yaml");
|
|
12
|
+
if (!fs.existsSync(p))
|
|
13
|
+
return [];
|
|
14
|
+
const doc = parseUserYaml(fs.readFileSync(p, "utf8"), p);
|
|
15
|
+
return doc?.workers ?? [];
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=read.js.map
|
package/dist/register-all.js
CHANGED
|
@@ -82,6 +82,7 @@ import { registerSearchCommand } from "./commands/search.js";
|
|
|
82
82
|
import { registerIndexCommand } from "./commands/index-cmd.js";
|
|
83
83
|
import { registerDoctorCommand } from "./commands/doctor.js";
|
|
84
84
|
import { registerMeshCommand } from "./commands/mesh.js";
|
|
85
|
+
import { registerBotCommand } from "./commands/bot.js";
|
|
85
86
|
/** Register the complete hq command graph onto `program`. */
|
|
86
87
|
export function registerAllCommands(program) {
|
|
87
88
|
// Module management subcommand group
|
|
@@ -232,5 +233,6 @@ export function registerAllCommands(program) {
|
|
|
232
233
|
// Work mesh (subcommand group — `hq mesh …`). Native REST + cache. Distinct
|
|
233
234
|
// from `hq doctor` (hook guardrails). Does not start MQTT listen.
|
|
234
235
|
registerMeshCommand(program);
|
|
236
|
+
registerBotCommand(program);
|
|
235
237
|
}
|
|
236
238
|
//# sourceMappingURL=register-all.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.111.0",
|
|
4
4
|
"description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"!dist/**/*.map",
|
|
13
13
|
"assets/scaffold",
|
|
14
|
+
"assets/bot-workers",
|
|
14
15
|
"assets/mesh-daemon",
|
|
15
16
|
"CHANGELOG.md"
|
|
16
17
|
],
|