@clearlysh/beehaven 0.8.16
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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/__tests__/_cli-switch.js +105 -0
- package/dist/__tests__/agent-wrap-probe.js +136 -0
- package/dist/agent-attribution.js +192 -0
- package/dist/agent-identity-daemon.js +135 -0
- package/dist/agent-onboard.js +156 -0
- package/dist/agent-scope.js +79 -0
- package/dist/agent-sessions.js +370 -0
- package/dist/agent-state.js +482 -0
- package/dist/app-manifest.js +221 -0
- package/dist/app-signer.js +54 -0
- package/dist/app-signing.js +127 -0
- package/dist/audio-play.js +98 -0
- package/dist/avatar-protocol.js +25 -0
- package/dist/avatar-route.js +79 -0
- package/dist/bee/cloud-proxy.js +130 -0
- package/dist/blocked-actions.js +120 -0
- package/dist/build-channel.js +101 -0
- package/dist/buildings/daemon-workspace.js +475 -0
- package/dist/claude-bridge.js +310 -0
- package/dist/claude-credential-store.js +633 -0
- package/dist/clearly-sync.js +518 -0
- package/dist/clearly.js +616 -0
- package/dist/cli-state.js +334 -0
- package/dist/cli.js +6973 -0
- package/dist/commands/agent-skills.js +302 -0
- package/dist/commands/agent.js +1310 -0
- package/dist/commands/app.js +1123 -0
- package/dist/commands/auth.js +171 -0
- package/dist/commands/building-shell.js +392 -0
- package/dist/commands/canvas.js +932 -0
- package/dist/commands/claude.js +218 -0
- package/dist/commands/compliance.js +387 -0
- package/dist/commands/compose.js +507 -0
- package/dist/commands/composition.js +95 -0
- package/dist/commands/connect-session.js +409 -0
- package/dist/commands/conversations.js +160 -0
- package/dist/commands/costs.js +443 -0
- package/dist/commands/d1.js +253 -0
- package/dist/commands/daemon-cmd.js +162 -0
- package/dist/commands/deploy.js +164 -0
- package/dist/commands/design.js +617 -0
- package/dist/commands/dev-generate.js +125 -0
- package/dist/commands/dev.js +109 -0
- package/dist/commands/devices.js +135 -0
- package/dist/commands/explain-router.js +24 -0
- package/dist/commands/explain.js +882 -0
- package/dist/commands/explore.js +264 -0
- package/dist/commands/help.js +2418 -0
- package/dist/commands/init.js +408 -0
- package/dist/commands/install.js +129 -0
- package/dist/commands/library.js +229 -0
- package/dist/commands/marketplace.js +138 -0
- package/dist/commands/mcp-install.js +476 -0
- package/dist/commands/mention.js +257 -0
- package/dist/commands/omni.js +529 -0
- package/dist/commands/secrets.js +323 -0
- package/dist/commands/self-update.js +206 -0
- package/dist/commands/sheet.js +264 -0
- package/dist/commands/shell-output.js +309 -0
- package/dist/commands/shell.js +1048 -0
- package/dist/commands/shop.js +368 -0
- package/dist/commands/skills.js +311 -0
- package/dist/commands/slides.js +418 -0
- package/dist/commands/swarm.js +341 -0
- package/dist/commands/ticket.js +1301 -0
- package/dist/commands/tool.js +312 -0
- package/dist/commands/trash.js +106 -0
- package/dist/commands/treehouse.js +120 -0
- package/dist/commands/user.js +240 -0
- package/dist/commands/voice-tts.js +319 -0
- package/dist/commands/voice.js +166 -0
- package/dist/commands/watch-git.js +401 -0
- package/dist/commands/webhook.js +159 -0
- package/dist/commands/workspace.js +3354 -0
- package/dist/composition-repos.js +91 -0
- package/dist/daemon-avatar.js +130 -0
- package/dist/daemon-claude-code-auto-install.js +52 -0
- package/dist/daemon-client.js +363 -0
- package/dist/daemon-codex.js +349 -0
- package/dist/daemon-entry.js +101 -0
- package/dist/daemon-fetch-allowlist.js +194 -0
- package/dist/daemon-gemini.js +224 -0
- package/dist/daemon-mcp-clearly.js +353 -0
- package/dist/daemon-mcp-sandbox.js +127 -0
- package/dist/daemon-omni-company.js +338 -0
- package/dist/daemon-omni-hooks.js +375 -0
- package/dist/daemon-omni-identity.js +229 -0
- package/dist/daemon-omni-pipeline.js +251 -0
- package/dist/daemon-omni-policies.js +346 -0
- package/dist/daemon-omni-principal.js +580 -0
- package/dist/daemon-omni-tool-gate.js +116 -0
- package/dist/daemon-omni.js +1442 -0
- package/dist/daemon-path-transform.js +120 -0
- package/dist/daemon-write-sandbox.js +191 -0
- package/dist/daemon.js +4471 -0
- package/dist/dashboard.js +846 -0
- package/dist/dep-installer.js +286 -0
- package/dist/dev-mode.js +67 -0
- package/dist/doc-indexer.js +169 -0
- package/dist/e2e-crypto.js +94 -0
- package/dist/feature-flags.js +154 -0
- package/dist/file-browser.js +88 -0
- package/dist/first-party-hives.js +211 -0
- package/dist/folder-sync.js +226 -0
- package/dist/health.js +56 -0
- package/dist/ipc-auth.js +299 -0
- package/dist/ipc-client.js +495 -0
- package/dist/ipc-server.js +684 -0
- package/dist/keychain.js +224 -0
- package/dist/lib/admin-client.js +73 -0
- package/dist/lib/mobile-build-parser.js +156 -0
- package/dist/local-app-manager.js +660 -0
- package/dist/log-redaction.js +89 -0
- package/dist/logger.js +108 -0
- package/dist/manifest.js +143 -0
- package/dist/marketplace.js +793 -0
- package/dist/min-version-copy.js +51 -0
- package/dist/min-version.js +214 -0
- package/dist/public-surface.js +23 -0
- package/dist/relay-presets.js +89 -0
- package/dist/relay.js +2723 -0
- package/dist/repl.js +619 -0
- package/dist/setup-relay.js +363 -0
- package/dist/setup-service.js +536 -0
- package/dist/shared/bee-memory/index.js +6 -0
- package/dist/shared/bee-memory/types.js +682 -0
- package/dist/shared/claude-code/daemon-types.js +112 -0
- package/dist/shared/claude-code/index.js +2 -0
- package/dist/shared/claude-code/types.js +1 -0
- package/dist/shared/status/schema.js +133 -0
- package/dist/shell-session.js +198 -0
- package/dist/speak-lock.js +97 -0
- package/dist/spinner.js +57 -0
- package/dist/telemetry.js +166 -0
- package/dist/terminal/block-parser.js +207 -0
- package/dist/terminal/shell-integration.js +108 -0
- package/dist/transcript-tailer.js +215 -0
- package/dist/tts-qwen3.js +547 -0
- package/dist/types.js +4 -0
- package/dist/utm.js +38 -0
- package/dist/verb-surface.js +331 -0
- package/dist/version-utils.js +85 -0
- package/dist/watcher.js +207 -0
- package/package.json +88 -0
- package/python/qwen3_tts.py +230 -0
- package/scripts/stamp-build-channel.mjs +115 -0
- package/scripts/sync-installed.mjs +159 -0
- package/scripts/sync-skills.mjs +112 -0
- package/skills/README.md +24 -0
- package/skills/agent-doodle-space/SKILL.md +118 -0
- package/skills/agent-home/SKILL.md +98 -0
- package/skills/beehaven-cli/SKILL.md +144 -0
- package/skills/claude-shopper/README.md +90 -0
- package/skills/claude-shopper/SKILL.md +151 -0
- package/skills/clearly-agent/SKILL.md +183 -0
- package/skills/clearly-canvas/SKILL.md +222 -0
- package/skills/clearly-canvas/authoring.md +160 -0
- package/skills/clearly-canvas/communications.md +32 -0
- package/skills/clearly-canvas/composition-patterns.md +84 -0
- package/skills/clearly-canvas/example.mjs +78 -0
- package/skills/clearly-canvas/project-plans.md +156 -0
- package/skills/clearly-canvas/review.mjs +68 -0
- package/skills/clearly-workspace/SKILL.md +79 -0
- package/skills/clearly-workspace/survey.mjs +101 -0
- package/skills/codebase-map/SKILL.md +142 -0
- package/skills/design-craft/SKILL.md +300 -0
- package/skills/index.json +79 -0
- package/skills/layout-systems/SKILL.md +184 -0
- package/skills/pair-on-canvas/SKILL.md +92 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven omni` — DAEMON OMNI: Claude Code hosted in the daemon, with the
|
|
3
|
+
* beehaven surface as first-party tools.
|
|
4
|
+
*
|
|
5
|
+
* ## How this differs from `beehaven claude`
|
|
6
|
+
*
|
|
7
|
+
* `beehaven claude` runs the Agent SDK in the CLI's OWN process — one Claude
|
|
8
|
+
* per terminal invocation, dying with the command, with no beehaven tools and
|
|
9
|
+
* no way for anything else to see it. That is a fine shape for a one-shot task
|
|
10
|
+
* and a bad one for a companion.
|
|
11
|
+
*
|
|
12
|
+
* This drives the session living in the DAEMON. It is the same runtime the
|
|
13
|
+
* avatar's chat panel talks to, so a turn started here is visible there, and
|
|
14
|
+
* both share one warm session, one conversation, one set of tools. The daemon
|
|
15
|
+
* is also the only process holding the relay socket and the local buildings —
|
|
16
|
+
* which is what lets `mcp__clearly__*` be a real in-process tool call instead
|
|
17
|
+
* of a shell-out.
|
|
18
|
+
*
|
|
19
|
+
* ## Local only, and that is deliberate
|
|
20
|
+
*
|
|
21
|
+
* Everything here rides the IPC socket, which is chmod 0600 — the kernel
|
|
22
|
+
* proving the caller is this UNIX user, driving their own Claude subscription.
|
|
23
|
+
* Cloud→daemon Claude orchestration stays disabled (see relay.ts, Anthropic
|
|
24
|
+
* credential-use policy). Nothing in this file opens that path.
|
|
25
|
+
*
|
|
26
|
+
* beehaven omni "add a red rectangle to my canvas"
|
|
27
|
+
* beehaven omni --session build "run the tests" # a named thread
|
|
28
|
+
* beehaven omni sessions # what's live
|
|
29
|
+
* beehaven omni end [--session id]
|
|
30
|
+
*/
|
|
31
|
+
import { IpcClient } from '../ipc-client.js';
|
|
32
|
+
import { color } from '../spinner.js';
|
|
33
|
+
const DEFAULT_SESSION = 'default';
|
|
34
|
+
export async function cmdOmni(positionals, values) {
|
|
35
|
+
// `positionals[0]` is the verb itself ('omni') — the CLI passes the full
|
|
36
|
+
// argv slice, matching `commands/claude.ts`. Reading index 0 as the
|
|
37
|
+
// subcommand makes `beehaven omni sessions` prompt Claude with the literal
|
|
38
|
+
// words "omni sessions", which it will then dutifully go and research.
|
|
39
|
+
const sub = positionals[1];
|
|
40
|
+
const sessionId = String(values.session ?? DEFAULT_SESSION);
|
|
41
|
+
const client = new IpcClient();
|
|
42
|
+
try {
|
|
43
|
+
await client.connect();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.error(color.red(' ✗ Cannot reach the daemon.'));
|
|
47
|
+
console.error(color.dim(' Start it with `beehaven start --headless`, or open the Mac app.'));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
if (sub === 'sessions') {
|
|
53
|
+
const res = await client.omni('sessions');
|
|
54
|
+
const list = res?.sessions ?? [];
|
|
55
|
+
console.log(` beehaven tools: ${res?.beehavenTools ? color.green('mounted') : color.yellow('not mounted')}`);
|
|
56
|
+
if (!list.length) {
|
|
57
|
+
console.log(color.dim(' No live sessions.'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const s of list) {
|
|
61
|
+
console.log(` ${color.cyan(s.sessionId)} turns=${s.turns ?? 0} cwd=${color.dim(s.cwd ?? '')}`);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* ⚠⚠ WHERE IS IT WORKING, AND MOVE IT. The cwd decides which files the agent reads and writes
|
|
67
|
+
* and which SDK conversation resumes — and until this there was no way to see it or change it,
|
|
68
|
+
* so a session started in the wrong directory stayed there for its whole life.
|
|
69
|
+
*
|
|
70
|
+
* beehaven omni cwd → print it
|
|
71
|
+
* beehaven omni cwd ~/projects/foo → move it
|
|
72
|
+
*/
|
|
73
|
+
/**
|
|
74
|
+
* ⚠⚠ COMPANY MODE — READ ITS STATE, AND SWITCH IT.
|
|
75
|
+
*
|
|
76
|
+
* `companyDiagnostics` was written to catch the two failures that produce no error and no
|
|
77
|
+
* output: the mode silently off because the env var never reached the DAEMON, and the daemon
|
|
78
|
+
* drifted onto the wrong workspace because `beehaven connect` is machine-global. Both had
|
|
79
|
+
* happened. Neither could be seen, because the diagnostic had no caller at all — the daemon
|
|
80
|
+
* handled `omni:company-status`, `ipc-server` routed it, and the documented invocation
|
|
81
|
+
* (`beehaven call --ipc …`) is not a real flag. This is that caller.
|
|
82
|
+
*
|
|
83
|
+
* beehaven omni company → the full report
|
|
84
|
+
* beehaven omni company on [--workspace <id>] → persisted, survives a daemon restart
|
|
85
|
+
* beehaven omni company off
|
|
86
|
+
*/
|
|
87
|
+
if (sub === 'company') {
|
|
88
|
+
const verb = String(positionals[2] ?? '').trim().toLowerCase();
|
|
89
|
+
if (verb === 'on' || verb === 'off') {
|
|
90
|
+
/**
|
|
91
|
+
* ⚠ WRITTEN BY THE CLI, READ BY THE DAEMON — one file, two processes, and no restart. That
|
|
92
|
+
* is the whole point: an env var could only be changed by respawning the daemon, which is
|
|
93
|
+
* why the mode had never actually been on.
|
|
94
|
+
*/
|
|
95
|
+
const { writeCompanyMode, COMPANY_MODE_FILE } = await import('../daemon-omni-company.js');
|
|
96
|
+
const wsId = String(values.workspace ?? '').trim();
|
|
97
|
+
const rec = writeCompanyMode({ enabled: verb === 'on', ...(wsId ? { workspaceId: wsId } : {}) });
|
|
98
|
+
console.log(` ${color.green('✓')} company mode ${rec.enabled ? color.green('ON') : color.yellow('OFF')}`);
|
|
99
|
+
if (rec.workspaceId)
|
|
100
|
+
console.log(color.dim(` workspace ${rec.workspaceId}`));
|
|
101
|
+
console.log(color.dim(` ${COMPANY_MODE_FILE}`));
|
|
102
|
+
/**
|
|
103
|
+
* ⚠ THE DAEMON READS THIS PER TURN, so it takes effect on the next one — no restart. Said
|
|
104
|
+
* out loud because every other switch in this CLI has needed one, and silently working is
|
|
105
|
+
* indistinguishable from silently not.
|
|
106
|
+
*/
|
|
107
|
+
console.log(color.dim(' Takes effect on the next turn. `beehaven omni company` to confirm.'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (verb && verb !== 'status') {
|
|
111
|
+
console.error(color.red(` ✗ unknown: beehaven omni company ${verb}`));
|
|
112
|
+
console.error(color.dim(' Try: beehaven omni company [status|on|off]'));
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const d = await client.omni('company-status');
|
|
117
|
+
const on = d?.mode === 'on';
|
|
118
|
+
console.log(` Company mode: ${on ? color.green('ON') : color.yellow('OFF')}${d?.source ? color.dim(` (${d.source})`) : ''}`);
|
|
119
|
+
const w = d?.workspace ?? {};
|
|
120
|
+
// ⚠ THREE STATES, NOT TWO. `matches === null` means "could not be checked" and must not be
|
|
121
|
+
// drawn as a mismatch — reporting a fault it cannot actually see is the worse failure.
|
|
122
|
+
const mark = w.matches === true ? color.green('✓') : w.matches === false ? color.red('✗') : color.yellow('?');
|
|
123
|
+
console.log(` Workspace: ${mark} expected ${color.cyan(w.expected ?? '?')}, connected ${w.connected ? color.cyan(w.connected) : color.dim('(unknown)')}`);
|
|
124
|
+
console.log(` Identity: ${d?.identityRegistered ? color.green('registered') : color.red('MISSING')}`);
|
|
125
|
+
console.log(` Claude: ${d?.claudeBinary ? color.dim(d.claudeBinary) : color.red('not found')}`);
|
|
126
|
+
const p = d?.prompt ?? {};
|
|
127
|
+
console.log(color.dim(` Prompt: ${p.totalBytes ?? 0} B (identity ${p.identityBytes ?? 0} + addon ${p.addonBytes ?? 0}) ≈ ${p.approxTokens ?? 0} tok/turn`));
|
|
128
|
+
/**
|
|
129
|
+
* ⚠⚠ THE ADDON NAMES VERBS, AND A NAME IS NOT A VERB. This check exists because the addon
|
|
130
|
+
* shipped telling the agent to call `ticket-link` — which does not exist; the action is
|
|
131
|
+
* `ticket-link-create`. Nothing could have caught that: the prompt is a string, so it
|
|
132
|
+
* type-checks, reads correctly, and fails only when a model is mid-sprint and gets
|
|
133
|
+
* "Unknown action" back. `describe-action` refuses an unregistered name, so asking it once
|
|
134
|
+
* per verb is a real existence check.
|
|
135
|
+
*
|
|
136
|
+
* ⚠ IT NEEDS A CONNECTED WORKSPACE, and says so rather than reporting a false absence — a
|
|
137
|
+
* diagnostic that prints "12 verbs missing" because the daemon is pointed at nothing is
|
|
138
|
+
* worse than one that prints nothing.
|
|
139
|
+
*/
|
|
140
|
+
const verbs = Array.isArray(d?.requiredVerbs) ? d.requiredVerbs : [];
|
|
141
|
+
if (verbs.length) {
|
|
142
|
+
const checks = await Promise.all(verbs.map(async (v) => {
|
|
143
|
+
try {
|
|
144
|
+
const r = await client.call('describe-action', { name: v }, 15_000);
|
|
145
|
+
// ⚠ An `unknown_action` code is the refusal; anything else that answers is real.
|
|
146
|
+
return { v, ok: !(r?.code === 'unknown_action' || /^Unknown action/i.test(String(r?.error ?? ''))), reached: true };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return { v, ok: false, reached: false };
|
|
150
|
+
}
|
|
151
|
+
}));
|
|
152
|
+
const unreachable = checks.filter((c) => !c.reached).length;
|
|
153
|
+
const missing = checks.filter((c) => c.reached && !c.ok).map((c) => c.v);
|
|
154
|
+
if (unreachable === checks.length) {
|
|
155
|
+
console.log(color.dim(` Verbs: ${verbs.length} named by the addon (not checked — no workspace reachable)`));
|
|
156
|
+
}
|
|
157
|
+
else if (missing.length) {
|
|
158
|
+
console.log(` Verbs: ${color.red(`${missing.length} of ${verbs.length} DO NOT EXIST`)}`);
|
|
159
|
+
for (const m of missing)
|
|
160
|
+
console.log(` ${color.red('✗')} ${m}`);
|
|
161
|
+
console.log(color.dim(' The addon tells the agent to call these. Fix the name in COMPANY_REQUIRED_VERBS and the addon text.'));
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.log(` Verbs: ${color.green(`${verbs.length}/${verbs.length}`)} named by the addon, all reachable`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const warn of d?.warnings ?? [])
|
|
168
|
+
console.log(` ${color.yellow('!')} ${warn}`);
|
|
169
|
+
if (!(d?.warnings ?? []).length)
|
|
170
|
+
console.log(` ${color.green('✓')} nothing to report`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (sub === 'cwd') {
|
|
174
|
+
const target = positionals.slice(2).join(' ').trim();
|
|
175
|
+
if (!target) {
|
|
176
|
+
const res = await client.omni('sessions');
|
|
177
|
+
const row = (res?.sessions ?? []).find((r) => r.sessionId === sessionId);
|
|
178
|
+
if (!row) {
|
|
179
|
+
console.log(color.dim(` no session "${sessionId}" yet — it is created on the first turn`));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.log(` ${color.cyan(sessionId)} ${row.cwd ?? color.dim('(unknown)')}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const r = await client.omni('set-cwd', { sessionId, cwd: target });
|
|
186
|
+
if (!r?.ok) {
|
|
187
|
+
console.error(color.red(` ✗ ${r?.error ?? 'could not change the working directory'}`));
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(` ${color.green('✓')} ${r.cwd}`);
|
|
192
|
+
/**
|
|
193
|
+
* ⚠ SAID OUT LOUD. Claude Code keys a conversation to a project directory derived from the
|
|
194
|
+
* cwd, so moving it starts a new thread. Someone who is not told simply finds an agent with
|
|
195
|
+
* no memory of the last five minutes.
|
|
196
|
+
*/
|
|
197
|
+
if (r.threadReset)
|
|
198
|
+
console.log(color.dim(' new conversation — the thread is tied to the directory'));
|
|
199
|
+
else
|
|
200
|
+
console.log(color.dim(' unchanged — the conversation is kept'));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (sub === 'end') {
|
|
204
|
+
await client.omni('end', { sessionId });
|
|
205
|
+
console.log(color.dim(` session ${sessionId} ended`));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
// ── A turn ────────────────────────────────────────────────────────
|
|
209
|
+
const prompt = positionals.slice(sub === 'run' ? 2 : 1).join(' ').trim();
|
|
210
|
+
if (!prompt) {
|
|
211
|
+
console.error(' Usage: beehaven omni "<task>" · beehaven omni sessions|end');
|
|
212
|
+
process.exitCode = 1;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
// Subscribe BEFORE prompting. The daemon starts emitting the moment the
|
|
216
|
+
// turn is accepted, and a subscription set up afterwards races the first
|
|
217
|
+
// tokens — you would lose the opening of every reply.
|
|
218
|
+
let printedAny = false;
|
|
219
|
+
let lastLineWasTool = false;
|
|
220
|
+
/**
|
|
221
|
+
* callId → tool name. The daemon deliberately leaves `tool` EMPTY on
|
|
222
|
+
* `agent:tool-result` ("the human-readable name lives on the matching
|
|
223
|
+
* tool-call event"), so a result can only be labelled by correlating on
|
|
224
|
+
* `callId` — which matters now that results render, because calls interleave
|
|
225
|
+
* and an unlabelled ✓ under the wrong step is worse than no label.
|
|
226
|
+
*/
|
|
227
|
+
const toolNames = new Map();
|
|
228
|
+
/** The most recent call, so a result printed right under it can stay unlabelled. */
|
|
229
|
+
let lastCallId = '';
|
|
230
|
+
const done = new Promise((resolve) => {
|
|
231
|
+
client.on('broadcast', (msg) => {
|
|
232
|
+
if (!msg?.omni || msg.sessionId !== sessionId)
|
|
233
|
+
return;
|
|
234
|
+
switch (msg.type) {
|
|
235
|
+
/**
|
|
236
|
+
* ⚠ THE REPLY. `reply_to_principal` and the other principal tools —
|
|
237
|
+
* this is what Omni deliberately chose to say.
|
|
238
|
+
*
|
|
239
|
+
* Rendering this was the whole parity gap. Without it a terminal user
|
|
240
|
+
* saw `agent:text-delta` (below) and nothing else, which is exactly
|
|
241
|
+
* backwards: that stream is Omni's PRIVATE working text, which the
|
|
242
|
+
* desktop chat hides on purpose, while the actual replies went
|
|
243
|
+
* nowhere. A CLI user was reading the agent's notes and missing its
|
|
244
|
+
* answers.
|
|
245
|
+
*/
|
|
246
|
+
case 'agent:principal': {
|
|
247
|
+
const p = msg.event ?? {};
|
|
248
|
+
if (lastLineWasTool) {
|
|
249
|
+
process.stdout.write('\n');
|
|
250
|
+
lastLineWasTool = false;
|
|
251
|
+
}
|
|
252
|
+
if (p.kind === 'prose') {
|
|
253
|
+
process.stdout.write(`\n${p.text ?? ''}\n`);
|
|
254
|
+
printedAny = true;
|
|
255
|
+
}
|
|
256
|
+
else if (p.kind === 'workspace') {
|
|
257
|
+
// Cards, design docs, memories — real changes, but not the answer.
|
|
258
|
+
console.log(color.dim(` · ${p.text ?? ''}`));
|
|
259
|
+
}
|
|
260
|
+
else if (p.kind === 'preview') {
|
|
261
|
+
// No window to open, so hand over the URL. This is the one place a
|
|
262
|
+
// terminal is allowed the thing the desktop role forbids — there,
|
|
263
|
+
// "go and open this" hands work back; here it IS the surface.
|
|
264
|
+
console.log(` ${color.cyan(String(p.url ?? ''))} ${color.dim(String(p.caption ?? ''))}`);
|
|
265
|
+
}
|
|
266
|
+
else if (p.kind === 'image') {
|
|
267
|
+
console.log(color.dim(` [image] ${String(p.caption ?? '')}`));
|
|
268
|
+
}
|
|
269
|
+
else if (p.kind === 'window') {
|
|
270
|
+
/**
|
|
271
|
+
* ⚠ FLOATING WINDOWS ARE THE ONE THING A CLI USER DOES NOT GET,
|
|
272
|
+
* and it says so rather than failing silently. The daemon emits the
|
|
273
|
+
* request; only the Mac app can perform it.
|
|
274
|
+
*/
|
|
275
|
+
console.log(color.dim(` · (window ${p.op}: ${p.target} — not available in the terminal)`));
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* A question the turn is BLOCKED on. Answered inline, because the
|
|
281
|
+
* alternative is a terminal that hangs for ten minutes and then
|
|
282
|
+
* reports a timeout the user had no way to prevent.
|
|
283
|
+
*/
|
|
284
|
+
case 'agent:ask':
|
|
285
|
+
void answerInteractively(client, msg.askId, msg.ask);
|
|
286
|
+
break;
|
|
287
|
+
/**
|
|
288
|
+
* ⚠ NOT THE REPLY — Omni's own working text, STREAMED as it is produced.
|
|
289
|
+
*
|
|
290
|
+
* Dimmed rather than hidden, and no longer behind `--verbose`. On a
|
|
291
|
+
* terminal there is no work-log drawer to open, so a two-minute coding
|
|
292
|
+
* turn that prints nothing until it finishes is indistinguishable from a
|
|
293
|
+
* hang — which is the single most common reason someone Ctrl-Cs live
|
|
294
|
+
* work. The dimming is what keeps it from being mistaken for the answer.
|
|
295
|
+
*
|
|
296
|
+
* `--quiet` suppresses it for scripts, where interleaved thinking is
|
|
297
|
+
* noise rather than reassurance.
|
|
298
|
+
*/
|
|
299
|
+
case 'agent:text-delta':
|
|
300
|
+
if (!values.quiet) {
|
|
301
|
+
if (lastLineWasTool) {
|
|
302
|
+
process.stdout.write('\n');
|
|
303
|
+
lastLineWasTool = false;
|
|
304
|
+
}
|
|
305
|
+
process.stdout.write(color.dim(msg.text ?? ''));
|
|
306
|
+
printedAny = true;
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
/**
|
|
310
|
+
* ⚠ STEPS RENDER INLINE — the name, then EVERY argument, then the result.
|
|
311
|
+
*
|
|
312
|
+
* This used to be one line per call: the tool name plus `summarize()`,
|
|
313
|
+
* which picked a single string field and cut it at 80 chars. So
|
|
314
|
+
* `canvas_act {action:'canvas.compose', args:{kind:'hero',…}}` rendered
|
|
315
|
+
* as the composition id and nothing else — you could see THAT a step
|
|
316
|
+
* happened and never what it did. Reading back a session meant guessing.
|
|
317
|
+
*/
|
|
318
|
+
case 'agent:tool-call': {
|
|
319
|
+
const callName = String(msg.tool ?? '');
|
|
320
|
+
// Recorded even when quiet, so a result that DOES print (a failure)
|
|
321
|
+
// can still name its tool.
|
|
322
|
+
toolNames.set(String(msg.callId ?? ''), callName);
|
|
323
|
+
if (values.quiet)
|
|
324
|
+
break;
|
|
325
|
+
lastCallId = String(msg.callId ?? '');
|
|
326
|
+
process.stdout.write(`\n ${color.dim('▪')} ${color.cyan(callName)}\n`);
|
|
327
|
+
/**
|
|
328
|
+
* ⚠ SOME TOOLS' ARGUMENTS ARE THE OUTPUT, and printing both says it
|
|
329
|
+
* twice. `reply_to_principal`'s `text` IS the answer, which arrives a
|
|
330
|
+
* moment later as `agent:principal` and renders in full — so dumping
|
|
331
|
+
* the arg first shows the whole reply dimmed, then again brightly. The
|
|
332
|
+
* step line still prints, because knowing Omni chose to reply is
|
|
333
|
+
* useful; only the echo is dropped.
|
|
334
|
+
*/
|
|
335
|
+
if (ECHOED_BY_EVENT.has(callName)) {
|
|
336
|
+
lastLineWasTool = false;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
for (const line of describeInput(msg.input ?? {})) {
|
|
340
|
+
// ⚠ THE NEWLINE STAYS OUTSIDE THE COLOUR. Wrapping it puts the reset
|
|
341
|
+
// sequence AFTER the line break, so the next line starts mid-escape
|
|
342
|
+
// and a piped log shows a stray `[0m` at every line start.
|
|
343
|
+
process.stdout.write(`${color.dim(` ${line}`)}\n`);
|
|
344
|
+
}
|
|
345
|
+
lastLineWasTool = false;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* ⚠ SUCCESSES ARE SHOWN TOO, WHICH THEY WERE NOT.
|
|
350
|
+
*
|
|
351
|
+
* The old rule was failures-only, on the reasoning that "a successful
|
|
352
|
+
* grep's stdout would bury the reply". That reasoning was sound and the
|
|
353
|
+
* conclusion was too strong: the daemon already caps `preview` at 200
|
|
354
|
+
* chars before it ever leaves the DO, so the flood it guarded against
|
|
355
|
+
* cannot reach here. What the rule actually cost was the ability to see
|
|
356
|
+
* whether a step did what it claimed — a tool that returns `{ok:false}`
|
|
357
|
+
* INSIDE a successful call (which every canvas RPC does) was invisible.
|
|
358
|
+
*/
|
|
359
|
+
case 'agent:tool-result': {
|
|
360
|
+
// ⚠ FAILURES SURVIVE `--quiet`. A script that suppresses the step log
|
|
361
|
+
// still needs to see the step that broke; silence there turns a failed
|
|
362
|
+
// run into a mysteriously empty one.
|
|
363
|
+
if (values.quiet && msg.ok)
|
|
364
|
+
break;
|
|
365
|
+
/**
|
|
366
|
+
* ⚠ THE NAME IS ONLY PRINTED WHEN IT IS NOT ALREADY OBVIOUS.
|
|
367
|
+
*
|
|
368
|
+
* A result normally lands directly under its own call, where repeating
|
|
369
|
+
* the tool name is pure noise on every single step. It stops being
|
|
370
|
+
* obvious when calls INTERLEAVE — parallel tool use means the result
|
|
371
|
+
* above may belong to a different step — so the label appears exactly
|
|
372
|
+
* then, which is the only time it carries information.
|
|
373
|
+
*/
|
|
374
|
+
const callId = String(msg.callId ?? '');
|
|
375
|
+
const name = callId === lastCallId ? '' : (toolNames.get(callId) ?? '');
|
|
376
|
+
const mark = msg.ok ? color.green('✓') : color.red('✗');
|
|
377
|
+
const body = String(msg.preview ?? '').replace(/\s+/g, ' ').trim();
|
|
378
|
+
const shown = body.length > 160 ? `${body.slice(0, 159)}…` : body;
|
|
379
|
+
const tint = msg.ok ? color.dim : color.red;
|
|
380
|
+
process.stdout.write(` ${mark} ${tint(shown || (msg.ok ? 'ok' : 'failed'))}${name ? color.dim(` (${name})`) : ''}\n`);
|
|
381
|
+
lastLineWasTool = false;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
case 'agent:error':
|
|
385
|
+
console.error(`\n ${color.red('✗')} ${msg.error}`);
|
|
386
|
+
resolve();
|
|
387
|
+
break;
|
|
388
|
+
case 'agent:done':
|
|
389
|
+
process.stdout.write('\n');
|
|
390
|
+
resolve();
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
await client.omni('prompt', { prompt, sessionId, cwd: values.cwd ?? process.cwd() });
|
|
396
|
+
// No timeout on the wait: a coding turn legitimately runs for minutes and
|
|
397
|
+
// the user can always Ctrl-C. A timer here would abandon live work.
|
|
398
|
+
await done;
|
|
399
|
+
if (!printedAny)
|
|
400
|
+
console.log(color.dim(' (no text in that turn)'));
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
console.error(color.red(` ✗ ${err?.message ?? err}`));
|
|
404
|
+
process.exitCode = 1;
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
client.disconnect();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Render a blocked question and send the answer back.
|
|
412
|
+
*
|
|
413
|
+
* ⚠ THE TURN IS PARKED WHILE THIS IS OPEN. `ask_principal` and `propose_plan`
|
|
414
|
+
* sit inside a tool call in the daemon waiting for `omni:answer`, so a terminal
|
|
415
|
+
* that printed the question and moved on would hang for the full ten-minute
|
|
416
|
+
* timeout and then be told nobody answered.
|
|
417
|
+
*
|
|
418
|
+
* ⚠ FAILS AS A DISMISSAL, NOT AS AN ANSWER. If stdin is not a TTY (a script, a
|
|
419
|
+
* pipe, CI) there is nobody to ask, and answering with an empty string would
|
|
420
|
+
* read to the agent as a choice. `dismissed` is the honest outcome and the agent
|
|
421
|
+
* is instructed to treat it as "not now" rather than consent.
|
|
422
|
+
*/
|
|
423
|
+
async function answerInteractively(client, askId, ask) {
|
|
424
|
+
const opts = Array.isArray(ask?.options) ? ask.options : [];
|
|
425
|
+
try {
|
|
426
|
+
console.log(`\n ${color.yellow('?')} ${ask?.question ?? 'Omni has a question'}`);
|
|
427
|
+
if (ask?.detail)
|
|
428
|
+
console.log(color.dim(` ${String(ask.detail).split('\n').join('\n ')}`));
|
|
429
|
+
if (!process.stdin.isTTY) {
|
|
430
|
+
console.log(color.dim(' (not a terminal — skipping)'));
|
|
431
|
+
await client.omni('answer', { askId, value: '', outcome: 'dismissed' });
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const { createInterface } = await import('node:readline/promises');
|
|
435
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
436
|
+
try {
|
|
437
|
+
let value = '';
|
|
438
|
+
if (opts.length) {
|
|
439
|
+
opts.forEach((o, i) => {
|
|
440
|
+
console.log(` ${color.cyan(String(i + 1))}. ${o.label}${o.detail ? color.dim(` — ${o.detail}`) : ''}`);
|
|
441
|
+
});
|
|
442
|
+
const raw = (await rl.question(' Choose [1]: ')).trim();
|
|
443
|
+
// Empty means the first option, which is where Omni is told to put its
|
|
444
|
+
// recommendation — so pressing return agrees with the recommendation.
|
|
445
|
+
const idx = raw === '' ? 0 : Number(raw) - 1;
|
|
446
|
+
value = opts[Number.isFinite(idx) && idx >= 0 && idx < opts.length ? idx : 0].value;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
value = (await rl.question(' ')).trim();
|
|
450
|
+
}
|
|
451
|
+
await client.omni('answer', { askId, value, outcome: value ? 'answered' : 'dismissed' });
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
rl.close();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Never let a prompt failure strand the turn — dismiss so it can continue.
|
|
459
|
+
try {
|
|
460
|
+
await client.omni('answer', { askId, value: '', outcome: 'dismissed' });
|
|
461
|
+
}
|
|
462
|
+
catch { /* gone */ }
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Every argument of a tool call, one `key: value` per line.
|
|
467
|
+
*
|
|
468
|
+
* ⚠ REPLACES `summarize()`, which returned ONE line built from the first string
|
|
469
|
+
* field it recognised. That was fine for `Bash{command}` and actively misleading
|
|
470
|
+
* for a structured call: `canvas_act{action, args, compositionId}` printed only
|
|
471
|
+
* the composition id, so the step that composed a hero block and the step that
|
|
472
|
+
* deleted a node looked identical.
|
|
473
|
+
*
|
|
474
|
+
* Bounded on BOTH axes on purpose — a `Write` call carries an entire file and a
|
|
475
|
+
* `MultiEdit` carries a list of them, so an unbounded dump would push the reply
|
|
476
|
+
* off screen, which is the failure the old one-liner was over-correcting for.
|
|
477
|
+
* Long values are cut at 240 chars; long inputs stop after 12 keys and say how
|
|
478
|
+
* many were withheld rather than trailing off silently.
|
|
479
|
+
*/
|
|
480
|
+
const MAX_VALUE_CHARS = 240;
|
|
481
|
+
const MAX_KEYS = 12;
|
|
482
|
+
/**
|
|
483
|
+
* Tools whose ARGUMENTS are re-rendered by a dedicated event, so echoing them as
|
|
484
|
+
* a step would print the same content twice.
|
|
485
|
+
*
|
|
486
|
+
* Each of these emits an `agent:principal` payload that the renderer above draws
|
|
487
|
+
* properly — prose, a URL, an image caption, a window notice. `ask_principal` is
|
|
488
|
+
* here for the same reason via `agent:ask`, which prints the question AND the
|
|
489
|
+
* choices before blocking on an answer.
|
|
490
|
+
*/
|
|
491
|
+
const ECHOED_BY_EVENT = new Set([
|
|
492
|
+
'mcp__principal__reply_to_principal',
|
|
493
|
+
'mcp__principal__ask_principal',
|
|
494
|
+
'mcp__principal__show_preview',
|
|
495
|
+
'mcp__principal__show_image',
|
|
496
|
+
'mcp__principal__open_window',
|
|
497
|
+
]);
|
|
498
|
+
function describeInput(input) {
|
|
499
|
+
const entries = Object.entries(input ?? {});
|
|
500
|
+
if (!entries.length)
|
|
501
|
+
return [];
|
|
502
|
+
const lines = [];
|
|
503
|
+
for (const [k, v] of entries.slice(0, MAX_KEYS)) {
|
|
504
|
+
let s;
|
|
505
|
+
if (typeof v === 'string')
|
|
506
|
+
s = v;
|
|
507
|
+
else if (v === null || v === undefined)
|
|
508
|
+
s = String(v);
|
|
509
|
+
else {
|
|
510
|
+
// Objects/arrays are the whole reason this exists — serialize rather than
|
|
511
|
+
// letting them render as "[object Object]", the exact bug this repo already
|
|
512
|
+
// records for `json` columns that are a type hint and not a serializer.
|
|
513
|
+
try {
|
|
514
|
+
s = JSON.stringify(v);
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
s = String(v);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
s = s.replace(/\s+/g, ' ').trim();
|
|
521
|
+
if (s.length > MAX_VALUE_CHARS)
|
|
522
|
+
s = `${s.slice(0, MAX_VALUE_CHARS - 1)}…`;
|
|
523
|
+
lines.push(`${k}: ${s}`);
|
|
524
|
+
}
|
|
525
|
+
const hidden = entries.length - MAX_KEYS;
|
|
526
|
+
if (hidden > 0)
|
|
527
|
+
lines.push(`… ${hidden} more field${hidden === 1 ? '' : 's'}`);
|
|
528
|
+
return lines;
|
|
529
|
+
}
|