@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,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `beehaven sheet` — spreadsheets.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ WHY A VERB WHEN `call` ALREADY WORKED. Same argument `ticket.ts` makes: the six `sheet-*`
|
|
5
|
+
* actions had no CLI door at all, so every spreadsheet touched from a terminal was
|
|
6
|
+
* `beehaven call sheet-set '{…}'` — not by preference, but because there was nothing else. A
|
|
7
|
+
* first-class artifact with no verb is one nobody thinks to look for.
|
|
8
|
+
*
|
|
9
|
+
* ⚠ AND IT MAKES TWO TRAPS UNTYPEABLE, which is the only reason a wrapper earns its place here:
|
|
10
|
+
*
|
|
11
|
+
* · `sheet-set` takes `{ id, at, rows }` — an ANCHOR cell plus a row array — or `{ id, cells }`
|
|
12
|
+
* keyed by A1 reference. It does NOT take `range` + `tsv`; that is `sheet-get`'s shape, and
|
|
13
|
+
* sending it here is two unknown keys, which validation strips, leaving a call that returns
|
|
14
|
+
* ok having written nothing. ⚠ I wrote this verb wrong the first time on exactly that guess —
|
|
15
|
+
* caught by reading `explain sheet-set`, not by reasoning about the name.
|
|
16
|
+
* · `sheet-from-file` needs a workspace fileId, NOT a local path. Handing it `./data.csv` fails
|
|
17
|
+
* in a way that reads as the file being unreadable; the refusal here says to `teleport` first.
|
|
18
|
+
*
|
|
19
|
+
* ⚠ `agentRpc`, NEVER bare `rpcOnce` — a bare call runs as the ACCOUNT OWNER rather than as the
|
|
20
|
+
* signed-in agent, skipping `agent-invoke`, the per-agent gate and the activity log.
|
|
21
|
+
*/
|
|
22
|
+
import { agentRpc as rpcOnce } from './workspace.js';
|
|
23
|
+
const c = {
|
|
24
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
25
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
26
|
+
};
|
|
27
|
+
function getTimeout(values) {
|
|
28
|
+
const t = Number(values?.timeout);
|
|
29
|
+
return Number.isFinite(t) && t > 0 ? t : 30_000;
|
|
30
|
+
}
|
|
31
|
+
/** ⚠ `rpcOnce` returns `{ error }` as a VALUE and never throws — see `rpcFailure` in workspace.ts. */
|
|
32
|
+
function failure(res) {
|
|
33
|
+
if (!res || typeof res !== 'object')
|
|
34
|
+
return null;
|
|
35
|
+
if (typeof res.error === 'string' && res.error)
|
|
36
|
+
return res.error;
|
|
37
|
+
if (res.ok === false)
|
|
38
|
+
return String(res.message ?? res.code ?? 'the workspace refused the call');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* ⚠ `\n` AND `\t` ARRIVE LITERAL FROM A SHELL THAT DID NOT INTERPRET THEM. Single quotes, most
|
|
43
|
+
* `sh` builtins and every scripted caller pass the backslash through, so the documented example
|
|
44
|
+
* would otherwise store one cell containing `Region\tQ1`. Shared by `set` and `append` — they had
|
|
45
|
+
* drifted apart on exactly this, and a spreadsheet verb that disagrees with its sibling about what
|
|
46
|
+
* a tab is stores garbage that looks like data.
|
|
47
|
+
*/
|
|
48
|
+
function unescapeTsv(s) {
|
|
49
|
+
return s.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
|
|
50
|
+
}
|
|
51
|
+
function die(msg, hint) {
|
|
52
|
+
console.error(` ${msg}`);
|
|
53
|
+
if (hint)
|
|
54
|
+
console.error(c.dim(` ${hint}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
export async function cmdSheet(sub, rest, values) {
|
|
58
|
+
const t = getTimeout(values);
|
|
59
|
+
if (!sub || sub === 'help') {
|
|
60
|
+
console.log(`
|
|
61
|
+
${c.cyan('beehaven sheet')} — spreadsheets
|
|
62
|
+
|
|
63
|
+
sheet list Every sheet, newest first
|
|
64
|
+
sheet new "<title>" Create one (--project <id>)
|
|
65
|
+
sheet get <id> Read it (--range A1:D20)
|
|
66
|
+
sheet set <id> <at> <tsv> Write a block at a cell (<at> = TOP-LEFT, e.g. A1)
|
|
67
|
+
sheet append <id> <tsv> Add rows at the end
|
|
68
|
+
sheet import <fileId> Build a sheet from an uploaded CSV/spreadsheet
|
|
69
|
+
|
|
70
|
+
${c.dim('<id> is a sheet id or its key (e.g. CLR-42).')}
|
|
71
|
+
${c.dim('TSV: tab-separated cells, newline-separated rows. Quote the whole thing.')}
|
|
72
|
+
${c.dim('<at> is one cell, not a range — set writes a block starting there.')}
|
|
73
|
+
${c.dim('set REPLACES what it covers; append never overwrites. They are not synonyms.')}
|
|
74
|
+
`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (sub === 'list') {
|
|
78
|
+
const res = await rpcOnce('sheet-list', {}, t, values);
|
|
79
|
+
const f = failure(res);
|
|
80
|
+
if (f)
|
|
81
|
+
die(f);
|
|
82
|
+
if (values.json) {
|
|
83
|
+
console.log(JSON.stringify(res, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const rows = res?.sheets ?? [];
|
|
87
|
+
if (!rows.length) {
|
|
88
|
+
console.log(' No sheets.');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
for (const s of rows)
|
|
93
|
+
console.log(` ${c.cyan(String(s.key ?? s.id ?? ''))} ${String(s.title ?? 'Untitled')}`);
|
|
94
|
+
console.log('');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (sub === 'new' || sub === 'create') {
|
|
98
|
+
const title = rest.join(' ').trim();
|
|
99
|
+
if (!title)
|
|
100
|
+
die('a title is required', 'beehaven sheet new "Q1 revenue"');
|
|
101
|
+
/**
|
|
102
|
+
* ⚠⚠ `rows: []` IS LOAD-BEARING — `sheet-create` IS AN IMPORT VERB AND REFUSES A BARE TITLE.
|
|
103
|
+
*
|
|
104
|
+
* Measured: `{ title }` alone answers *"Nothing to import — pass csv, tsv, text, rows or
|
|
105
|
+
* localPath"* (`code: 'no-content'`). Reasonable for the job it was written for; wrong for the
|
|
106
|
+
* word "new", which to a person means an EMPTY spreadsheet they can then fill in.
|
|
107
|
+
*
|
|
108
|
+
* An empty array satisfies the `Array.isArray(input.rows)` branch, which returns before the
|
|
109
|
+
* content check — so it creates a real, empty sheet. Verified against the live worker.
|
|
110
|
+
*/
|
|
111
|
+
const res = await rpcOnce('sheet-create', {
|
|
112
|
+
title,
|
|
113
|
+
rows: [],
|
|
114
|
+
...(values.project ? { projectId: String(values.project) } : {}),
|
|
115
|
+
}, t, values);
|
|
116
|
+
const f = failure(res);
|
|
117
|
+
if (f)
|
|
118
|
+
die(f);
|
|
119
|
+
if (values.json) {
|
|
120
|
+
console.log(JSON.stringify(res, null, 2));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
console.log(` Created ${c.cyan(String(res?.key ?? res?.id ?? ''))}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (sub === 'get') {
|
|
127
|
+
const id = rest[0];
|
|
128
|
+
if (!id)
|
|
129
|
+
die('a sheet id or key is required', 'beehaven sheet get CLR-42');
|
|
130
|
+
const res = await rpcOnce('sheet-get', {
|
|
131
|
+
id,
|
|
132
|
+
...(values.range ? { range: String(values.range) } : {}),
|
|
133
|
+
}, t, values);
|
|
134
|
+
const f = failure(res);
|
|
135
|
+
if (f)
|
|
136
|
+
die(f);
|
|
137
|
+
if (values.json) {
|
|
138
|
+
console.log(JSON.stringify(res, null, 2));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* ⚠⚠ THE DATA IS `cells`. `rows` IS A COUNT, AND READING IT LOOKS LIKE AN EMPTY SHEET.
|
|
143
|
+
*
|
|
144
|
+
* `sheet-get` answers `{ cells: [[…]], rows: 4, cols: 2, range: "A1:B4" }` — so `res.rows` is
|
|
145
|
+
* the NUMBER 4, `.length` on it is `undefined`, and this printed "(empty)" for a sheet with
|
|
146
|
+
* four populated rows. Written from the field name, caught only by reading a live response.
|
|
147
|
+
* A confident empty answer is the worst failure available here: it reads as data loss.
|
|
148
|
+
*/
|
|
149
|
+
const rows = Array.isArray(res?.cells) ? res.cells : [];
|
|
150
|
+
// ⚠ TAB-SEPARATED, not a drawn table. The output of a spreadsheet read is data — a caller
|
|
151
|
+
// pipes it into `cut`/`awk`, and box-drawing characters make that a parsing job.
|
|
152
|
+
const tsv = rows.map((r) => r.map((cell) => String(cell ?? '')).join('\t')).join('\n');
|
|
153
|
+
/**
|
|
154
|
+
* ⚠⚠ IT ACCEPTED `--out` AND WROTE NOTHING (CBH2-85). `--out`/`-o` are global in `parseArgs`,
|
|
155
|
+
* so this verb took the flag and printed to stdout anyway — the accept-and-ignore class this
|
|
156
|
+
* sweep keeps finding. `planDeckExport` is REUSED rather than re-implemented: it already owns
|
|
157
|
+
* the clobber refusal and the extension fallback, both sabotage-tested, and a second copy is
|
|
158
|
+
* how one verb comes to refuse an overwrite while its sibling silently performs one.
|
|
159
|
+
*
|
|
160
|
+
* ⚠ THE EMPTY CASE IS HANDLED **AFTER** THE WRITE DECISION, DELIBERATELY. Returning early on
|
|
161
|
+
* `!rows.length` would make `--out` on an empty sheet print "(empty)" and write nothing —
|
|
162
|
+
* accept-and-ignore again, in the one case a caller cannot distinguish from a failed read.
|
|
163
|
+
* An empty sheet writes an empty file, which is the honest answer.
|
|
164
|
+
*/
|
|
165
|
+
const outPath = (values.out ?? values.output);
|
|
166
|
+
if (outPath) {
|
|
167
|
+
const { writeFileSync, existsSync } = await import('fs');
|
|
168
|
+
const { resolve } = await import('path');
|
|
169
|
+
const { planDeckExport } = await import('./slides.js');
|
|
170
|
+
const plan = planDeckExport(
|
|
171
|
+
// ⚠ A SYNTHETIC PAYLOAD, because the planner is shaped by the RESULT, not by the verb —
|
|
172
|
+
// `{ content, filename }` is the same shape `deck-export`'s markdown returns.
|
|
173
|
+
{ content: tsv, filename: `${res?.key ?? id}.tsv` }, { format: 'tsv', out: outPath, id: String(id), force: !!values.force }, existsSync, resolve);
|
|
174
|
+
if (!plan.ok)
|
|
175
|
+
die(plan.error, plan.hint);
|
|
176
|
+
for (const file of plan.files) {
|
|
177
|
+
const buf = Buffer.from(file.body.kind === 'text' ? file.body.content : '', 'utf8');
|
|
178
|
+
writeFileSync(file.dest, buf);
|
|
179
|
+
console.log(` Wrote ${c.cyan(file.dest)}${c.dim(` · ${buf.length} bytes · ${rows.length} rows`)}`);
|
|
180
|
+
}
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!rows.length) {
|
|
184
|
+
console.log(' (empty)');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
console.log(tsv);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (sub === 'set') {
|
|
191
|
+
/**
|
|
192
|
+
* ⚠⚠ `at` + `rows`, NOT `range` + `tsv` — AND I WROTE IT THE WRONG WAY FIRST.
|
|
193
|
+
*
|
|
194
|
+
* The obvious guess from the verb name is a RANGE (`A1:C2`) and a blob of text, which is what
|
|
195
|
+
* `sheet-get` takes. `sheet-set` takes a top-left ANCHOR (`at`) plus a `rows` array, or a
|
|
196
|
+
* `cells` map keyed by A1 reference. Sending `{ range, tsv }` is two unknown keys: validation
|
|
197
|
+
* strips them, `id` alone is valid, and the call returns ok having written nothing.
|
|
198
|
+
*
|
|
199
|
+
* This verb exists to make that untypeable, so it takes the anchor and does the parse.
|
|
200
|
+
*/
|
|
201
|
+
const [id, at, ...tsvParts] = rest;
|
|
202
|
+
if (!id || !at) {
|
|
203
|
+
die('usage: beehaven sheet set <id> <at> <tsv>', 'e.g. beehaven sheet set CLR-42 A1 "Region\\tQ1\\nAPAC\\t7" — `at` is the TOP-LEFT cell, not a range.');
|
|
204
|
+
}
|
|
205
|
+
const tsv = tsvParts.join(' ');
|
|
206
|
+
if (!tsv)
|
|
207
|
+
die('nothing to write', 'Pass TSV: tab-separated cells, newline-separated rows.');
|
|
208
|
+
// ⚠ `\n` and `\t` arrive as literal backslash-n from a shell that did not interpret them;
|
|
209
|
+
// accept both so the documented example works whichever quoting the caller used.
|
|
210
|
+
const rows = unescapeTsv(tsv).split('\n').map((line) => line.split('\t'));
|
|
211
|
+
const res = await rpcOnce('sheet-set', { id, at, rows }, t, values);
|
|
212
|
+
const f = failure(res);
|
|
213
|
+
if (f)
|
|
214
|
+
die(f);
|
|
215
|
+
if (values.json) {
|
|
216
|
+
console.log(JSON.stringify(res, null, 2));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
console.log(` Wrote ${rows.length} row${rows.length === 1 ? '' : 's'} at ${c.cyan(at)} in ${c.cyan(id)}`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (sub === 'append') {
|
|
223
|
+
const [id, ...tsvParts] = rest;
|
|
224
|
+
if (!id)
|
|
225
|
+
die('a sheet id or key is required', 'beehaven sheet append CLR-42 "row\\tvalues"');
|
|
226
|
+
const tsv = tsvParts.join(' ');
|
|
227
|
+
if (!tsv)
|
|
228
|
+
die('nothing to append');
|
|
229
|
+
// ⚠ SAME ESCAPE HANDLING AS `set` — without it `append "LATAM\t4"` stored the literal
|
|
230
|
+
// backslash-t as ONE cell. A shell that did not interpret the escape hands it through
|
|
231
|
+
// verbatim, and only `set` was expanding it, so the two verbs disagreed about their input.
|
|
232
|
+
const res = await rpcOnce('sheet-append', { id, tsv: unescapeTsv(tsv) }, t, values);
|
|
233
|
+
const f = failure(res);
|
|
234
|
+
if (f)
|
|
235
|
+
die(f);
|
|
236
|
+
if (values.json) {
|
|
237
|
+
console.log(JSON.stringify(res, null, 2));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
console.log(` Appended to ${c.cyan(id)}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (sub === 'import') {
|
|
244
|
+
const fileId = rest[0];
|
|
245
|
+
if (!fileId) {
|
|
246
|
+
die('a workspace fileId is required', 'Upload first: beehaven teleport ./data.csv → then pass the id it prints.');
|
|
247
|
+
}
|
|
248
|
+
const res = await rpcOnce('sheet-from-file', {
|
|
249
|
+
fileId,
|
|
250
|
+
...(values.title ? { title: String(values.title) } : {}),
|
|
251
|
+
...(values.project ? { projectId: String(values.project) } : {}),
|
|
252
|
+
}, t, values);
|
|
253
|
+
const f = failure(res);
|
|
254
|
+
if (f)
|
|
255
|
+
die(f);
|
|
256
|
+
if (values.json) {
|
|
257
|
+
console.log(JSON.stringify(res, null, 2));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
console.log(` Created ${c.cyan(String(res?.key ?? res?.id ?? ''))}`);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
die(`unknown subcommand "${sub}"`, 'beehaven sheet help');
|
|
264
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the shell verbs PRINT — extracted from `shell.ts` so the decision is testable.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ THE DEFECT THIS MODULE EXISTS FOR IS DISCARD, NOT ABSENCE. Every fact these commands failed
|
|
5
|
+
* to print was already in the RPC result and was dropped on the way to the terminal:
|
|
6
|
+
*
|
|
7
|
+
* mv "~/nope.md" "~/x.md" -> { ok:false, error:'no such file', code:'enoent', hint:'Nothing at …' }
|
|
8
|
+
* mkdir "~/Probe" -> { ok:true, folder:'~/Probe', id:'0bfecca9-…', created:true }
|
|
9
|
+
* rm "~/Probe/OP-3.md" -> { ok:true, archived:['~/…'], note:'moved to Trash — restore …' }
|
|
10
|
+
*
|
|
11
|
+
* …and `execNotesShell` read only `stdout`, `rows` and `stderr`, so ALL of the above printed
|
|
12
|
+
* nothing at all. A failed `mv` and a successful one were byte-identical on screen.
|
|
13
|
+
*
|
|
14
|
+
* ⚠⚠ SO THE RULE IS GENERIC, NEVER A PER-VERB TEMPLATE. `summariseResult` renders whatever
|
|
15
|
+
* non-envelope fields came back, in the order the server sent them. A template per verb would
|
|
16
|
+
* have to be extended for every future verb, and the verb it was NOT extended for goes back to
|
|
17
|
+
* printing nothing — which is exactly the failure mode being fixed. Nothing here invents a field:
|
|
18
|
+
* if the server said it, it is printed; if it did not, it is absent.
|
|
19
|
+
*
|
|
20
|
+
* ⚠ AND A FAILURE IS NEVER SILENT, whatever shape it arrives in. `formatShellResult` falls back to
|
|
21
|
+
* a compact JSON dump of the result when `ok === false` carries no message, because an unreadable
|
|
22
|
+
* error is recoverable and an invisible one is not.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Fields that are transport plumbing or an echo of the request — never news.
|
|
26
|
+
*
|
|
27
|
+
* ⚠ `command` AND `cwd` ARE AN ECHO, NOT A RESULT. The notes shell returns both on every call;
|
|
28
|
+
* printing them would repeat the line the caller just typed, under the breadcrumb that already
|
|
29
|
+
* repeats it.
|
|
30
|
+
*
|
|
31
|
+
* ⚠ `stdout`/`stderr`/`rows` ARE RENDERED SEPARATELY, and must not also appear in the summary —
|
|
32
|
+
* `sql` would print its whole result set twice.
|
|
33
|
+
*/
|
|
34
|
+
export const ENVELOPE_FIELDS = new Set([
|
|
35
|
+
'ok', 'type', 'requestId', 'command', 'cwd', 'stdout', 'stderr', 'rows', 'exitCode',
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Fields that are a SENTENCE rather than a value, so they get their own line.
|
|
39
|
+
*
|
|
40
|
+
* `rm`'s `note` ("moved to Trash — restore from the gallery, or with document-restore") is the
|
|
41
|
+
* single most load-bearing thing the organise verbs return, and inlining it into a `·`-joined
|
|
42
|
+
* summary buries it.
|
|
43
|
+
*/
|
|
44
|
+
const PROSE_FIELDS = new Set(['note', 'hint', 'warning', 'message']);
|
|
45
|
+
const MAX_LIST_ITEMS = 3;
|
|
46
|
+
function renderValue(v) {
|
|
47
|
+
if (Array.isArray(v)) {
|
|
48
|
+
const shown = v.slice(0, MAX_LIST_ITEMS).map((x) => (typeof x === 'string' ? x : JSON.stringify(x)));
|
|
49
|
+
const more = v.length > MAX_LIST_ITEMS ? ` (+${v.length - MAX_LIST_ITEMS} more)` : '';
|
|
50
|
+
return `${shown.join(', ')}${more}`;
|
|
51
|
+
}
|
|
52
|
+
if (v && typeof v === 'object')
|
|
53
|
+
return JSON.stringify(v);
|
|
54
|
+
return String(v);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A one-line confirmation built from the fields the server actually returned.
|
|
58
|
+
*
|
|
59
|
+
* ⚠ A `true` BOOLEAN RENDERS AS ITS BARE NAME (`created`, `existed`) and a `false` one is dropped.
|
|
60
|
+
* `created true` reads as a field dump; `created` reads as an outcome — and `mkdir` distinguishing
|
|
61
|
+
* `created` from `existed` is the whole difference between "I made that" and "it was already
|
|
62
|
+
* there", which the caller currently cannot see at all.
|
|
63
|
+
*
|
|
64
|
+
* Returns `''` when there is genuinely nothing beyond the envelope. Silence is only correct when
|
|
65
|
+
* nothing was discarded.
|
|
66
|
+
*/
|
|
67
|
+
export function summariseResult(r) {
|
|
68
|
+
const empty = { line: '', prose: [] };
|
|
69
|
+
if (!r || typeof r !== 'object' || Array.isArray(r))
|
|
70
|
+
return empty;
|
|
71
|
+
const parts = [];
|
|
72
|
+
const prose = [];
|
|
73
|
+
for (const [k, v] of Object.entries(r)) {
|
|
74
|
+
if (ENVELOPE_FIELDS.has(k))
|
|
75
|
+
continue;
|
|
76
|
+
if (v === undefined || v === null || v === '')
|
|
77
|
+
continue;
|
|
78
|
+
if (v === false)
|
|
79
|
+
continue;
|
|
80
|
+
if (PROSE_FIELDS.has(k)) {
|
|
81
|
+
prose.push(String(v));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (v === true) {
|
|
85
|
+
parts.push(k);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(v) && v.length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
parts.push(`${k} ${renderValue(v)}`);
|
|
91
|
+
}
|
|
92
|
+
return { line: parts.join(' · '), prose };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* ⚠⚠ A HINT THAT NAMES A CAUSE MUST NOT BE RELAYED WHEN THE PAYLOAD CARRIES NO EVIDENCE FOR IT —
|
|
96
|
+
* CBH2-52.
|
|
97
|
+
*
|
|
98
|
+
* $ beehaven edit ~/CH-1.md draft final # CH-1 has NEVER contained "draft"
|
|
99
|
+
* edit: find text not present
|
|
100
|
+
* hint They edited this note after you last wrote to it — see changedByThem for what
|
|
101
|
+
* moved, then retry with the passage as it now reads.
|
|
102
|
+
*
|
|
103
|
+
* The first line is correct. The hint asserts a SPECIFIC cause — a concurrent editor — that is
|
|
104
|
+
* false here: the string was never in the document, and the workspace has one author. There is no
|
|
105
|
+
* `changedByThem` in the payload to look at either.
|
|
106
|
+
*
|
|
107
|
+
* ⚠⚠ IT IS WORSE THAN NO HINT, WHICH IS THE WHOLE POINT. It sends the reader to diff a document
|
|
108
|
+
* against an edit that never happened, and on a shared workspace it INVENTS a collaborator
|
|
109
|
+
* conflict — a social fact, out of a typo. Same class as CBH2-61 and CBH2-51: an error that names
|
|
110
|
+
* the wrong cause is more expensive than one that names none, because it is actionable and the
|
|
111
|
+
* action is wasted.
|
|
112
|
+
*
|
|
113
|
+
* THE RULE, and it generalises past this one message: **a hint that tells you to go and look at a
|
|
114
|
+
* named field is only true if the field is there.** `see <field>` is a checkable claim, so it is
|
|
115
|
+
* checked against the payload rather than trusted. A hint that makes no such claim is relayed
|
|
116
|
+
* untouched — this suppresses false causes, not hints.
|
|
117
|
+
*
|
|
118
|
+
* ⚠ THE FIX IS HERE, NOT IN THE SERVER'S WORDING. `notes-edit` is right to offer the concurrent-
|
|
119
|
+
* edit story when it HAS the evidence, and this is the one place every shell verb's refusal passes
|
|
120
|
+
* through — so the check lands once instead of per verb, and a future hint of the same shape is
|
|
121
|
+
* covered on the day it is written.
|
|
122
|
+
*
|
|
123
|
+
* PURE and exported, so the decision is testable without a workspace.
|
|
124
|
+
*/
|
|
125
|
+
/**
|
|
126
|
+
* Does this receipt actually show something MOVING?
|
|
127
|
+
*
|
|
128
|
+
* ⚠⚠ PRESENCE IS NOT EVIDENCE, AND MEASURING IT WAS THE FIRST WRONG FIX. `changedByThem` is
|
|
129
|
+
* returned on EVERY `notes-edit` refusal, so a test for "is the field there" fires never. Measured
|
|
130
|
+
* on a freshly created document with one author and zero edits:
|
|
131
|
+
*
|
|
132
|
+
* changedByThem: { edits: 1, at: 1788702512564, unchanged: true }
|
|
133
|
+
*
|
|
134
|
+
* **`unchanged: true`** — the workspace has already worked out that nothing moved, and prints
|
|
135
|
+
* *"They edited this note after you last wrote to it"* on top of its own contrary finding. So the
|
|
136
|
+
* check has to read the receipt, not count it. `edits: 1` there is the document's own CREATION.
|
|
137
|
+
*/
|
|
138
|
+
function movedSomething(v) {
|
|
139
|
+
if (v === undefined || v === null || v === '')
|
|
140
|
+
return false;
|
|
141
|
+
if (Array.isArray(v))
|
|
142
|
+
return v.length > 0;
|
|
143
|
+
if (typeof v !== 'object')
|
|
144
|
+
return true;
|
|
145
|
+
const r = v;
|
|
146
|
+
if (Object.keys(r).length === 0)
|
|
147
|
+
return false;
|
|
148
|
+
if (r.unchanged === true)
|
|
149
|
+
return false; // it says so itself
|
|
150
|
+
const moved = (k) => Array.isArray(r[k]) && r[k].length > 0;
|
|
151
|
+
// ⚠ Only when the receipt HAS these keys. A differently-shaped receipt is not evidence of
|
|
152
|
+
// nothing — absent added/removed, a non-empty object that does not claim `unchanged` stands.
|
|
153
|
+
if ('added' in r || 'removed' in r)
|
|
154
|
+
return moved('added') || moved('removed');
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
export function vetHint(o, verb = '') {
|
|
158
|
+
const raw = typeof o.hint === 'string' ? o.hint.trim() : '';
|
|
159
|
+
if (!raw)
|
|
160
|
+
return null;
|
|
161
|
+
// The only claim shape worth policing: "…see <field> for what moved…" points at the payload.
|
|
162
|
+
const m = /\bsee\s+([a-z][A-Za-z0-9_]*)\b/.exec(raw);
|
|
163
|
+
if (!m)
|
|
164
|
+
return raw;
|
|
165
|
+
if (movedSomething(o[m[1]]))
|
|
166
|
+
return raw; // receipts — the hint stands
|
|
167
|
+
/**
|
|
168
|
+
* Unsupported. Say what IS known instead of what was guessed.
|
|
169
|
+
* ⚠ `not-found` gets a real next step; anything else gets SILENCE rather than an invented one —
|
|
170
|
+
* replacing one wrong cause with another is not an improvement.
|
|
171
|
+
*/
|
|
172
|
+
if (o.code === 'not-found') {
|
|
173
|
+
const what = verb === 'edit' ? 'The passage is not in the document' : 'It is not there';
|
|
174
|
+
return `${what} — it may never have been, or it may have changed. `
|
|
175
|
+
+ `Read it with \`cat\`, or \`grep\` for a phrase you are sure of, then retry with the exact text.`;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* What a failed shell call says.
|
|
181
|
+
*
|
|
182
|
+
* ⚠⚠ IT CAN NEVER RETURN AN EMPTY STRING. The server's `error`/`code`/`hint` triple is the normal
|
|
183
|
+
* case, but a refusal that arrives in some other shape must still reach the screen — hence the
|
|
184
|
+
* JSON fallback. "The command failed and said nothing" is the one outcome this whole module
|
|
185
|
+
* exists to make impossible.
|
|
186
|
+
*/
|
|
187
|
+
export function formatFailure(r, verb = '') {
|
|
188
|
+
const o = (r && typeof r === 'object' ? r : {});
|
|
189
|
+
const prefix = verb ? `${verb}: ` : '';
|
|
190
|
+
const msg = [o.error, o.message, o.stderr].find((x) => typeof x === 'string' && x.trim());
|
|
191
|
+
const lines = [];
|
|
192
|
+
if (msg) {
|
|
193
|
+
lines.push(`${prefix}${String(msg).replace(/\n+$/, '')}`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
// Nothing message-shaped came back. Show the whole thing rather than nothing.
|
|
197
|
+
const { line, prose } = summariseResult(o);
|
|
198
|
+
const detail = [line, ...prose].filter(Boolean).join(' · ');
|
|
199
|
+
lines.push(`${prefix}failed${detail ? ` — ${detail}` : ` — ${JSON.stringify(o)}`}`);
|
|
200
|
+
}
|
|
201
|
+
if (typeof o.code === 'string' && o.code.trim())
|
|
202
|
+
lines.push(` code ${o.code}`);
|
|
203
|
+
// ⚠ CBH2-52 — vetted, not relayed. See `vetHint`: a hint naming a cause the payload cannot
|
|
204
|
+
// support is worse than no hint, because it is actionable and the action is wasted.
|
|
205
|
+
const hint = vetHint(o, verb);
|
|
206
|
+
if (hint)
|
|
207
|
+
lines.push(` hint ${hint}`);
|
|
208
|
+
return `${lines.join('\n')}\n`;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The whole result → terminal mapping for a notes-shell call.
|
|
212
|
+
*
|
|
213
|
+
* ⚠ A STRUCTURED ANSWER IS STILL AN ANSWER. Most builtins write `stdout`; `sql` returns `rows`;
|
|
214
|
+
* the organise verbs return neither and were therefore invisible. All three are handled here so
|
|
215
|
+
* that one function can be asserted against every shape the shell produces.
|
|
216
|
+
*
|
|
217
|
+
* ⚠ A BARE `exit N` IS NOT A MESSAGE. The notes shell reports a non-zero exit as
|
|
218
|
+
* `stderr: "exit 1"`, and `grep` exits 1 when it matched nothing — real grep says nothing at all
|
|
219
|
+
* in that case. The code still rides on `exitCode`, so a script sees it and a person does not.
|
|
220
|
+
*/
|
|
221
|
+
export function formatShellResult(r, verb = '') {
|
|
222
|
+
const o = (r && typeof r === 'object' ? r : {});
|
|
223
|
+
const failed = o.ok === false;
|
|
224
|
+
const rawOut = String(o.stdout ?? '');
|
|
225
|
+
const rows = o.rows;
|
|
226
|
+
/**
|
|
227
|
+
* ⚠ JSON, NOT AN INVENTED TABLE. `beehaven call` already prints results this way, so a script
|
|
228
|
+
* can parse it and a person can read it; a second bespoke renderer here would be one more thing
|
|
229
|
+
* to keep in step with the shape the workspace actually returns.
|
|
230
|
+
*/
|
|
231
|
+
const structured = !rawOut && Array.isArray(rows) ? `${JSON.stringify(rows, null, 2)}\n` : '';
|
|
232
|
+
let stdout = rawOut || structured;
|
|
233
|
+
const stderrText = String(o.stderr ?? '');
|
|
234
|
+
const bareExit = /^exit \d+$/.test(stderrText.trim());
|
|
235
|
+
let stderr = '';
|
|
236
|
+
if (failed) {
|
|
237
|
+
// ⚠ `exit 1` from a no-match grep is not an error to report; anything else is.
|
|
238
|
+
stderr = bareExit && !o.error && !o.hint ? '' : formatFailure(o, verb);
|
|
239
|
+
}
|
|
240
|
+
else if (stderrText && !bareExit) {
|
|
241
|
+
stderr = `${stderrText.replace(/\n$/, '')}\n`;
|
|
242
|
+
}
|
|
243
|
+
// ⚠ ONLY WHEN THERE IS NO STREAM OUTPUT. `ls` returns both a `stdout` listing and a `cwd`; a
|
|
244
|
+
// summary beside a listing is noise, and the listing is what was asked for.
|
|
245
|
+
if (!failed && !stdout) {
|
|
246
|
+
const { line, prose } = summariseResult(o);
|
|
247
|
+
const all = [line, ...prose.map((p) => ` ${p}`)].filter(Boolean);
|
|
248
|
+
if (all.length)
|
|
249
|
+
stdout = `${all.join('\n')}\n`;
|
|
250
|
+
}
|
|
251
|
+
if (stdout && !stdout.endsWith('\n'))
|
|
252
|
+
stdout += '\n';
|
|
253
|
+
return { stdout, stderr, exitCode: failed ? 1 : 0 };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* ⚠⚠ `home` IS A ONE-WORD ADDRESS, AND THE BREADCRUMB WAS SPELLING IT `home/home`.
|
|
257
|
+
*
|
|
258
|
+
* `resolveDefaultBuilding` represents the default workspace as the tuple
|
|
259
|
+
* `{ buildingType: 'home', buildingId: 'home' }` — a deliberate special case, because `home` has
|
|
260
|
+
* no `<type>/<id>` shape. Rendering that tuple with a slash names a location that is not in the
|
|
261
|
+
* documented grammar (`home | <type>/<id> | <appName>`) and cannot be typed back into `cd`, while
|
|
262
|
+
* `beehaven call` prints the same target as plain `home` two lines earlier.
|
|
263
|
+
*/
|
|
264
|
+
export function breadcrumbAddress(buildingType, buildingId) {
|
|
265
|
+
if (buildingType === 'home')
|
|
266
|
+
return 'home';
|
|
267
|
+
return `${buildingType}/${buildingId}`;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* ⚠⚠ `grep foo ~/Dir` RECURSED AND HID THE FILENAMES — the flag said `-r` and the search did not.
|
|
271
|
+
*
|
|
272
|
+
* The shell builtin decides attribution with `showFilenames = recursive || nonFlags.length > 2`,
|
|
273
|
+
* where `recursive` means "the caller typed `-r`". But a DIRECTORY target auto-recurses anyway
|
|
274
|
+
* (its own comment: *"A recursive search over a directory must include the path so callers can
|
|
275
|
+
* locate matches"*), so a directory search produced matches from many documents with nothing to
|
|
276
|
+
* say which document any came from. Reproduced with three identical lines from three different
|
|
277
|
+
* notes.
|
|
278
|
+
*
|
|
279
|
+
* ⚠ THE SERVER ALREADY KNOWS HOW TO SEND THEM — it was never asked. Injecting `-r -n` asks, and
|
|
280
|
+
* the same door answers `~/Probe/OP-1 One.md:2:…`. Nothing here reformats the match: it is
|
|
281
|
+
* printed as received.
|
|
282
|
+
*
|
|
283
|
+
* ⚠ ONLY WHEN A PATH OPERAND IS PRESENT. With no path the builtin takes a different branch that
|
|
284
|
+
* ALREADY emits `path:line:text`; injecting flags there changes nothing and would make this look
|
|
285
|
+
* like it was doing more than it is.
|
|
286
|
+
*
|
|
287
|
+
* ⚠ AND ONLY FLAGS THE CALLER DID NOT TYPE. `-i`, `-l`, `-c`, `-v` keep working because the
|
|
288
|
+
* command still goes through the same builtin — routing to the structured `notes-grep` RPC would
|
|
289
|
+
* have silently dropped all four, which is the same class of defect in a new place. (`notes-grep`
|
|
290
|
+
* also defaults to case-INSENSITIVE, so that route would have quietly changed what matches.)
|
|
291
|
+
*/
|
|
292
|
+
export function grepAttributionFlags(args) {
|
|
293
|
+
const typed = new Set();
|
|
294
|
+
let operands = 0;
|
|
295
|
+
for (const a of args) {
|
|
296
|
+
if (a.startsWith('--'))
|
|
297
|
+
continue;
|
|
298
|
+
if (a.startsWith('-') && a.length > 1) {
|
|
299
|
+
for (const ch of a.slice(1))
|
|
300
|
+
typed.add(ch);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
operands++;
|
|
304
|
+
}
|
|
305
|
+
// `grep PATTERN` with no path — the builtin already attributes that branch.
|
|
306
|
+
if (operands < 2)
|
|
307
|
+
return [];
|
|
308
|
+
return ['r', 'n'].filter((f) => !typed.has(f)).map((f) => `-${f}`);
|
|
309
|
+
}
|