@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
package/dist/clearly.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `clearly` — THE PRODUCT CLI. Two verbs that do the work, and six that get out of their way.
|
|
4
|
+
*
|
|
5
|
+
* ⚠⚠ THIS IS NOT `beehaven`, AND THE SPLIT IS THE POINT. `beehaven` is the internal tool: ~50
|
|
6
|
+
* verbs, `admin`, `sql`, `d1`, a dev-mode escape hatch and a hidden "build your own backend"
|
|
7
|
+
* surface. Handing that to an engineering team to write documentation would ship them a console
|
|
8
|
+
* for a platform they are not buying. The precedent is already in this repo — `mwah` is the
|
|
9
|
+
* consumer CLI beside `beehaven` the developer one.
|
|
10
|
+
*
|
|
11
|
+
* ⚠ IT IS A FRONT-END, NOT A FORK. Every call goes through the SAME daemon over the SAME IPC
|
|
12
|
+
* socket, so login, the relay, DO routing and env selection are inherited rather than
|
|
13
|
+
* reimplemented. A second auth stack is how the two drift and how one of them ends up wrong.
|
|
14
|
+
*
|
|
15
|
+
* THE WORK note · ask
|
|
16
|
+
* THE ARCHIVE pull
|
|
17
|
+
* THE ACCOUNT login · logout · settings · status · uninstall
|
|
18
|
+
*
|
|
19
|
+
* ⚠ KEEP THIS LIST AND `help()` IN STEP. It said "three verbs, and deliberately only three" while
|
|
20
|
+
* the file had eight — two contradictory inventories in one document, which is the exact failure
|
|
21
|
+
* `thought-prompt.ts` shipped and had to be guarded against ("you have exactly ONE tool" while
|
|
22
|
+
* granting nineteen). A reader believes whichever they saw last.
|
|
23
|
+
*
|
|
24
|
+
* ⚠ GIT IS THE ONLY SOURCE TODAY, and that is a deliberate first step rather than a limitation to
|
|
25
|
+
* paper over. It is local, it needs no OAuth, and "reads your commits" is most of what the
|
|
26
|
+
* product promises. GitHub / Linear / Slack ingestion is a separate project; nothing here
|
|
27
|
+
* pretends otherwise, and `status` says so out loud.
|
|
28
|
+
*/
|
|
29
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { dirname, join, resolve } from 'node:path';
|
|
32
|
+
import { fileURLToPath } from 'node:url';
|
|
33
|
+
import { rpcOnce } from './ipc-client.js';
|
|
34
|
+
import { readCliConfig, patchCliConfig } from './feature-flags.js';
|
|
35
|
+
const argv = process.argv.slice(2);
|
|
36
|
+
const cmd = argv[0] ?? 'help';
|
|
37
|
+
const flag = (n) => {
|
|
38
|
+
const i = argv.indexOf(n);
|
|
39
|
+
return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : null;
|
|
40
|
+
};
|
|
41
|
+
const has = (n) => argv.includes(n);
|
|
42
|
+
const c = {
|
|
43
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
44
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
45
|
+
honey: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
46
|
+
mint: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
47
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* ⚠ THE AGENT TURN IS THE SLOW PART AND THE CLI RELAY CAPS AT 180s. `RESPOND_TIMEOUT_MS` on the
|
|
51
|
+
* server is the same 180s, so a turn that outlives this was going to be killed anyway — matching
|
|
52
|
+
* them means a timeout here always means the same thing it means there.
|
|
53
|
+
*/
|
|
54
|
+
const TURN_MS = 180_000;
|
|
55
|
+
/**
|
|
56
|
+
* ⚠ STDERR IS CAPTURED, NOT INHERITED. A failing probe is normal here — `HEAD~30` does not exist in
|
|
57
|
+
* a repo with two commits — and with inherited stderr git printed
|
|
58
|
+
* `fatal: ambiguous argument 'HEAD~30..HEAD'` straight into the user's terminal above a run that
|
|
59
|
+
* then succeeded. A tool that prints `fatal:` and works anyway teaches people to ignore the word.
|
|
60
|
+
*/
|
|
61
|
+
function git(args, cwd) {
|
|
62
|
+
try {
|
|
63
|
+
return execFileSync('git', args, {
|
|
64
|
+
cwd, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
|
|
65
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
66
|
+
}).trim();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return '';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function repoRoot() {
|
|
73
|
+
const root = git(['rev-parse', '--show-toplevel'], process.cwd());
|
|
74
|
+
return root || null;
|
|
75
|
+
}
|
|
76
|
+
const statePath = (root) => join(root, '.clearly', 'state.json');
|
|
77
|
+
function readState(root) {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(readFileSync(statePath(root), 'utf8'));
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function writeState(root, s) {
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(dirname(statePath(root)), { recursive: true });
|
|
88
|
+
writeFileSync(statePath(root), `${JSON.stringify(s, null, 2)}\n`);
|
|
89
|
+
}
|
|
90
|
+
catch { /* a lost bookmark costs a duplicate note, never the note itself */ }
|
|
91
|
+
}
|
|
92
|
+
/** True when the sha is a real commit in THIS repo — see the rebase note above. */
|
|
93
|
+
const commitExists = (root, sha) => !!sha && git(['cat-file', '-e', `${sha}^{commit}`], root) === '' && git(['rev-parse', '--verify', `${sha}^{commit}`], root) !== '';
|
|
94
|
+
/**
|
|
95
|
+
* What happened, as text the agent can reason about.
|
|
96
|
+
*
|
|
97
|
+
* ⚠⚠ THE DIFF IS SUMMARISED, NOT PASTED. A day of work is routinely tens of thousands of lines;
|
|
98
|
+
* sending it whole would blow the turn's context, cost real money per note, and bury the one
|
|
99
|
+
* thing being asked for. Subjects, bodies and a per-file stat are what a person reads to
|
|
100
|
+
* reconstruct a decision — the reasoning lives in commit messages and review comments, not in the
|
|
101
|
+
* hunks.
|
|
102
|
+
*
|
|
103
|
+
* ⚠ AND IT IS CAPPED, VISIBLY. Truncation that does not announce itself reads as "this is
|
|
104
|
+
* everything that happened", which is exactly the false confidence a decision record must not
|
|
105
|
+
* have.
|
|
106
|
+
*/
|
|
107
|
+
function collectWork(root, since) {
|
|
108
|
+
const fmt = '%h%x09%an%x09%ad%x09%s%n%b';
|
|
109
|
+
const log = since
|
|
110
|
+
? git(['log', `${since}..HEAD`, '--date=short', `--pretty=format:${fmt}`], root)
|
|
111
|
+
: git(['log', '-30', '--date=short', `--pretty=format:${fmt}`], root);
|
|
112
|
+
/**
|
|
113
|
+
* ⚠ THE DIFF BASE IS THE OLDEST COMMIT WE ACTUALLY READ, NOT `HEAD~30`. A repo with fewer than
|
|
114
|
+
* thirty commits has no `HEAD~30`, and asking for one made git print
|
|
115
|
+
* `fatal: ambiguous argument` while the run went on to succeed — the stat was simply empty and
|
|
116
|
+
* nobody could tell. Deriving the base from the log means the window always exists.
|
|
117
|
+
*/
|
|
118
|
+
const oldest = since || (() => {
|
|
119
|
+
const shas = git(['log', '-30', '--pretty=format:%H'], root).split('\n').filter(Boolean);
|
|
120
|
+
return shas.length ? `${shas[shas.length - 1]}^` : '';
|
|
121
|
+
})();
|
|
122
|
+
const stat = oldest
|
|
123
|
+
? git(['diff', '--stat', `${oldest}..HEAD`], root)
|
|
124
|
+
// No parent for the very first commit — show what that commit itself introduced.
|
|
125
|
+
: git(['show', '--stat', '--oneline', 'HEAD'], root);
|
|
126
|
+
const range = since ? `${since}..HEAD` : 'last 30 commits';
|
|
127
|
+
const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], root);
|
|
128
|
+
const commits = log ? log.split('\n').filter((l) => /^[0-9a-f]{7,}\t/.test(l)).length : 0;
|
|
129
|
+
const CAP = 12_000;
|
|
130
|
+
let text = [
|
|
131
|
+
`Repository: ${root.split('/').pop()}`,
|
|
132
|
+
`Branch: ${branch}`,
|
|
133
|
+
`Range: ${range}`,
|
|
134
|
+
'',
|
|
135
|
+
'## Commits',
|
|
136
|
+
log || '(none)',
|
|
137
|
+
'',
|
|
138
|
+
'## Files changed',
|
|
139
|
+
stat || '(none)',
|
|
140
|
+
].join('\n');
|
|
141
|
+
const truncated = text.length > CAP;
|
|
142
|
+
if (truncated)
|
|
143
|
+
text = `${text.slice(0, CAP)}\n\n[…truncated at ${CAP} characters]`;
|
|
144
|
+
return { text, commits, truncated };
|
|
145
|
+
}
|
|
146
|
+
async function turn(message) {
|
|
147
|
+
const res = await rpcOnce('agent-prompt', { message, wait: true, _timeout: TURN_MS }, TURN_MS);
|
|
148
|
+
const rows = Array.isArray(res?.toolCalls) ? res.toolCalls : [];
|
|
149
|
+
return {
|
|
150
|
+
reply: String(res?.reply ?? ''),
|
|
151
|
+
tools: rows.filter((r) => r?.role === 'tool_call').map((r) => String(r?.tool ?? '')),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function die(msg, hint) {
|
|
155
|
+
console.error(`\n ${c.red('✗')} ${msg}`);
|
|
156
|
+
if (hint)
|
|
157
|
+
console.error(` ${c.dim(hint)}`);
|
|
158
|
+
console.error('');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
// ── note ────────────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
async function cmdNote() {
|
|
163
|
+
const root = repoRoot();
|
|
164
|
+
if (!root)
|
|
165
|
+
die('Not a git repository.', 'clearly note reads your commits — run it inside a repo.');
|
|
166
|
+
/**
|
|
167
|
+
* ⚠ AN EXPLICIT `--since` ALWAYS WINS over the bookmark — re-noting a range on purpose is a
|
|
168
|
+
* thing people do (the first note was thin, the range was wrong), and a tool that silently
|
|
169
|
+
* refuses because it "already did that" is one you stop reaching for.
|
|
170
|
+
*/
|
|
171
|
+
const state = readState(root);
|
|
172
|
+
const bookmark = state.lastCommit && commitExists(root, state.lastCommit) ? state.lastCommit : null;
|
|
173
|
+
const since = flag('--since') ?? bookmark;
|
|
174
|
+
const head = git(['rev-parse', 'HEAD'], root);
|
|
175
|
+
if (!flag('--since') && bookmark && bookmark === head) {
|
|
176
|
+
console.log(`\n ${c.dim('Nothing new since the last note.')}`);
|
|
177
|
+
if (state.note)
|
|
178
|
+
console.log(` ${c.dim(state.note)}`);
|
|
179
|
+
console.log(` ${c.dim('Re-note a range anyway with:')} clearly note --since HEAD~5\n`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const { text, commits, truncated } = collectWork(root, since);
|
|
183
|
+
if (!commits) {
|
|
184
|
+
die('No commits found in that range.', since ? `Nothing between ${since} and HEAD.` : 'This repo has no history yet.');
|
|
185
|
+
}
|
|
186
|
+
if (bookmark && !flag('--since')) {
|
|
187
|
+
console.log(`\n ${c.dim(`since the last note (${bookmark.slice(0, 7)})`)}`);
|
|
188
|
+
}
|
|
189
|
+
if (has('--dry-run')) {
|
|
190
|
+
console.log(`\n${c.dim('— what would be sent —')}\n`);
|
|
191
|
+
console.log(text);
|
|
192
|
+
console.log(`\n${c.dim(`${commits} commits${truncated ? ' · truncated' : ''}`)}\n`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
console.log(`\n ${c.honey('nib')} ${c.dim(`reading ${commits} commits…`)}`);
|
|
196
|
+
/**
|
|
197
|
+
* ⚠⚠ THE BRIEF ASKS FOR WHAT WAS REJECTED, BECAUSE THAT IS THE PART THAT IS ALWAYS LOST.
|
|
198
|
+
* A summary of what changed is recoverable from the log forever; the option someone argued
|
|
199
|
+
* against and why is not, and it is what stops a team re-litigating the same thing next quarter.
|
|
200
|
+
*
|
|
201
|
+
* ⚠ AND IT FORBIDS INVENTION EXPLICITLY. The product's own FAQ promises a note leaves a section
|
|
202
|
+
* empty rather than filling it in, so the brief has to say so — a model asked for a tidy
|
|
203
|
+
* document will produce a tidy document whether or not the evidence is there.
|
|
204
|
+
*/
|
|
205
|
+
const brief = [
|
|
206
|
+
'Write ONE decision note from the work below, into the notes archive.',
|
|
207
|
+
'',
|
|
208
|
+
'Rules:',
|
|
209
|
+
'- The first line is the note title. Make it name the DECISION, not the task ("Retry backoff, exponential with jitter" — not "Update retry logic").',
|
|
210
|
+
'- Say WHY, from the evidence. Cite the commit hash or PR number that shows it.',
|
|
211
|
+
'- Record what was REJECTED and who argued against it, if the evidence shows it. This is the most valuable part.',
|
|
212
|
+
'- If the evidence does not support a section, LEAVE IT OUT. Do not invent a rationale.',
|
|
213
|
+
'- Look for an existing note on this subject first and update it rather than creating a near-duplicate.',
|
|
214
|
+
'- Reply with one line: the path you wrote and what changed. No preamble.',
|
|
215
|
+
'',
|
|
216
|
+
'--- the work ---',
|
|
217
|
+
text,
|
|
218
|
+
].join('\n');
|
|
219
|
+
const t0 = Date.now();
|
|
220
|
+
let out;
|
|
221
|
+
try {
|
|
222
|
+
out = await turn(brief);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const m = String(err?.message ?? err);
|
|
226
|
+
if (/timeout/i.test(m))
|
|
227
|
+
die('The note took too long to write.', 'Try a narrower range: clearly note --since HEAD~5');
|
|
228
|
+
die(m, 'clearly status — check the connection');
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* ⚠⚠ THE TOOL CALLS ARE THE EVIDENCE; THE REPLY IS ONLY A CLAIM.
|
|
232
|
+
*
|
|
233
|
+
* Observed on a real run: the turn grepped, globbed twice for nothing, hit a `bash` error, wrote
|
|
234
|
+
* NOTHING — and replied *"`~/Retry backoff…md` — recorded the exponential-backoff decision"*. The
|
|
235
|
+
* file did exist, from a different run seconds earlier, so the sentence read as true and the
|
|
236
|
+
* warning under it read as noise.
|
|
237
|
+
*
|
|
238
|
+
* A person reads the confident sentence. So when no `write` or `edit` ran, the CLI says so FIRST
|
|
239
|
+
* and does not print the claim as though it were the outcome — the same rule the eval suite
|
|
240
|
+
* already enforces for phantom board cards, applied to the verb this product is actually sold on.
|
|
241
|
+
* Reporting a note that does not exist is worse than reporting a failure: nobody goes looking for
|
|
242
|
+
* the second one.
|
|
243
|
+
*/
|
|
244
|
+
const wrote = out.tools.filter((t) => t === 'write' || t === 'edit').length;
|
|
245
|
+
const secs = ((Date.now() - t0) / 1000).toFixed(1);
|
|
246
|
+
if (!wrote) {
|
|
247
|
+
console.log(` ${c.dim(`${out.tools.length} steps · ${secs}s`)}\n`);
|
|
248
|
+
console.log(` ${c.red('✗')} ${c.bold('Nothing was written.')}`);
|
|
249
|
+
console.log(` ${c.dim('The agent looked but did not record anything. It said:')}\n`);
|
|
250
|
+
console.log(` ${c.dim(out.reply.trim().split('\n').join('\n ') || '(nothing)')}\n`);
|
|
251
|
+
console.log(` ${c.dim('If that reads like it worked, it did not — no write or edit ran.')}`);
|
|
252
|
+
console.log(` ${c.dim('Try a narrower range:')} clearly note --since HEAD~5\n`);
|
|
253
|
+
return; // ⚠ and the bookmark does NOT move — see below.
|
|
254
|
+
}
|
|
255
|
+
console.log(` ${c.mint('tuck')} ${c.dim(`filed · ${out.tools.length} steps · ${secs}s`)}\n`);
|
|
256
|
+
console.log(out.reply.trim() || c.dim('(the agent wrote nothing back)'));
|
|
257
|
+
/**
|
|
258
|
+
* ⚠ THE BOOKMARK MOVES ONLY WHEN SOMETHING WAS ACTUALLY WRITTEN. Advancing it on a turn that
|
|
259
|
+
* wrote nothing — a timeout, a refusal, an agent that decided the work was already covered —
|
|
260
|
+
* would skip that range forever, and the commits it describes would never be noted by anything.
|
|
261
|
+
* Losing a bookmark costs a duplicate; moving one wrongly costs the record.
|
|
262
|
+
*/
|
|
263
|
+
if (wrote && head) {
|
|
264
|
+
writeState(root, { lastCommit: head, at: new Date().toISOString(), note: out.reply.trim().slice(0, 200) });
|
|
265
|
+
}
|
|
266
|
+
console.log('');
|
|
267
|
+
}
|
|
268
|
+
// ── ask ─────────────────────────────────────────────────────────────────────────────────────
|
|
269
|
+
async function cmdAsk() {
|
|
270
|
+
const q = argv.slice(1).filter((a) => !a.startsWith('--')).join(' ').trim();
|
|
271
|
+
if (!q)
|
|
272
|
+
die('Ask what?', 'clearly ask "why did we drop fixed backoff?"');
|
|
273
|
+
console.log(`\n ${c.mint('tuck')} ${c.dim('searching the archive…')}`);
|
|
274
|
+
/**
|
|
275
|
+
* ⚠⚠ THE SEARCH IS DONE HERE, DETERMINISTICALLY, AND THE MODEL ONLY SYNTHESISES.
|
|
276
|
+
*
|
|
277
|
+
* The first version told the agent to search, and when it missed it answered "nothing in the
|
|
278
|
+
* archive covers that" about a note written ninety seconds earlier. The obvious fix — instruct
|
|
279
|
+
* it to try more phrasings — made it strictly worse: it re-issued the SAME search until the
|
|
280
|
+
* SDK's loop detector ended the turn and the person's answer was `Error: loop detected`.
|
|
281
|
+
*
|
|
282
|
+
* That is the same escalation this codebase already paid for once with the thought map's `type`
|
|
283
|
+
* filter, and it ended the same way: **a guard that depends on the model choosing correctly is
|
|
284
|
+
* not a guard.** Rewriting the instruction did not work there either; deleting the choice did.
|
|
285
|
+
*
|
|
286
|
+
* So retrieval moves out of the model. Grep is cheap, exact, and cannot loop; the model gets the
|
|
287
|
+
* hits and does the one thing it is actually better at — reading them and answering. It is also
|
|
288
|
+
* far faster, and a miss is now provably a miss rather than an agent that gave up.
|
|
289
|
+
*/
|
|
290
|
+
const stop = new Set(['what', 'why', 'when', 'where', 'which', 'does', 'did', 'the', 'and', 'for',
|
|
291
|
+
'that', 'this', 'with', 'have', 'has', 'was', 'are', 'our', 'you', 'how', 'not', 'but', 'from',
|
|
292
|
+
'about', 'into', 'they', 'them', 'their', 'were', 'been', 'would', 'could', 'should', 'we']);
|
|
293
|
+
const terms = q.toLowerCase().replace(/[^\w\s-]/g, ' ').split(/\s+/)
|
|
294
|
+
.filter((w) => w.length >= 4 && !stop.has(w));
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
const hits = [];
|
|
297
|
+
const searches = [];
|
|
298
|
+
/** The whole question first, then each distinctive word — narrowest signal to broadest. */
|
|
299
|
+
const probes = [q.replace(/[^\w\s-]/g, ' ').trim(), ...terms].slice(0, 8);
|
|
300
|
+
for (const p of probes) {
|
|
301
|
+
if (!p || hits.length >= 40)
|
|
302
|
+
continue;
|
|
303
|
+
searches.push(p);
|
|
304
|
+
try {
|
|
305
|
+
const r = await rpcOnce('notes-grep', { pattern: p, limit: 12 }, 60_000);
|
|
306
|
+
for (const m of (Array.isArray(r?.matches) ? r.matches : []).map(String)) {
|
|
307
|
+
if (seen.has(m))
|
|
308
|
+
continue;
|
|
309
|
+
seen.add(m);
|
|
310
|
+
hits.push(m);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch { /* one failed probe is not a failed search */ }
|
|
314
|
+
}
|
|
315
|
+
let out;
|
|
316
|
+
try {
|
|
317
|
+
/**
|
|
318
|
+
* ⚠⚠ ONE MISS IS NOT AN ANSWER, AND THIS BRIEF EXISTS BECAUSE OF A REAL ONE.
|
|
319
|
+
*
|
|
320
|
+
* Asked "why do the notes-shell tools have both prefixed and bare names?", the agent searched
|
|
321
|
+
* once, found nothing, and replied "Nothing in the notes archive covers that" — while a note
|
|
322
|
+
* written ninety seconds earlier said, on line 14, *"Bare names as the only registry names
|
|
323
|
+
* were rejected after six consecutive `Unknown action: bash` failures"*. `grep "bare names"`
|
|
324
|
+
* finds it instantly.
|
|
325
|
+
*
|
|
326
|
+
* **A false "we have no record of that" is worse than having no archive**: it sends someone off
|
|
327
|
+
* to re-litigate a decision that IS written down, and it teaches them the archive cannot be
|
|
328
|
+
* trusted — which is the whole asset.
|
|
329
|
+
*
|
|
330
|
+
* ⚠ THE ESCALATION IS SPELLED OUT because "search before you answer" plainly is not enough —
|
|
331
|
+
* it was already in the previous brief. A question and the note answering it are written by
|
|
332
|
+
* different people at different times, so the words rarely match: the asker says "why do the
|
|
333
|
+
* tools have two names", the note says "registry-prefixed … aliases". Searching the QUESTION's
|
|
334
|
+
* vocabulary and stopping is the single most likely way to miss.
|
|
335
|
+
*/
|
|
336
|
+
out = await turn([
|
|
337
|
+
'Answer this question from the workspace notes.',
|
|
338
|
+
'',
|
|
339
|
+
hits.length
|
|
340
|
+
? [
|
|
341
|
+
'These lines were already found for you by searching the archive. Read the notes they',
|
|
342
|
+
'point at if you need more context, then answer and CITE the note path.',
|
|
343
|
+
'If they genuinely do not answer it, say so — do not stretch them into an answer.',
|
|
344
|
+
'',
|
|
345
|
+
'--- search hits ---',
|
|
346
|
+
hits.slice(0, 40).join('\n'),
|
|
347
|
+
].join('\n')
|
|
348
|
+
: [
|
|
349
|
+
`The archive was searched for: ${searches.slice(0, 6).join(', ')} — nothing matched.`,
|
|
350
|
+
'Say plainly that it is not written down, and name what was searched for so they can',
|
|
351
|
+
'tell "not recorded" from "not found". Do not answer from general knowledge.',
|
|
352
|
+
].join('\n'),
|
|
353
|
+
'',
|
|
354
|
+
`--- question ---`,
|
|
355
|
+
q,
|
|
356
|
+
].join('\n'));
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
die(String(err?.message ?? err), 'clearly status — check the connection');
|
|
360
|
+
}
|
|
361
|
+
console.log('');
|
|
362
|
+
console.log(out.reply.trim() || c.dim('(no answer)'));
|
|
363
|
+
console.log('');
|
|
364
|
+
}
|
|
365
|
+
// ── pull ────────────────────────────────────────────────────────────────────────────────────
|
|
366
|
+
/**
|
|
367
|
+
* ⚠⚠ THIS IS THE CLAIM "your notes are files in your repo" MADE TRUE. Until the archive is on
|
|
368
|
+
* disk, "uninstall and the archive is untouched" is marketing rather than a property — the notes
|
|
369
|
+
* live in a Durable Object and leaving takes them with it.
|
|
370
|
+
*
|
|
371
|
+
* ⚠ ONE DIRECTION, DELIBERATELY. Writing local edits back is a merge problem with a live agent on
|
|
372
|
+
* the other side, and a half-built two-way sync loses work in a way nobody notices for weeks. Pull
|
|
373
|
+
* is safe, useful on its own (commit the archive, review it in a PR), and does not pretend.
|
|
374
|
+
*/
|
|
375
|
+
async function cmdPull() {
|
|
376
|
+
const root = repoRoot() ?? process.cwd();
|
|
377
|
+
const dir = resolve(flag('--dir') ?? join(root, '.clearly', 'notes'));
|
|
378
|
+
console.log(`\n ${c.dim('reading the archive…')}`);
|
|
379
|
+
const listed = await rpcOnce('notes-glob', { pattern: '~/**/*.md', limit: 500 }, 120_000);
|
|
380
|
+
const paths = Array.isArray(listed?.files) ? listed.files.map(String) : [];
|
|
381
|
+
if (!paths.length)
|
|
382
|
+
die('The archive is empty.', 'Write one first: clearly note');
|
|
383
|
+
let written = 0;
|
|
384
|
+
for (const p of paths) {
|
|
385
|
+
const rel = p.replace(/^~\/?/, '');
|
|
386
|
+
let body = '';
|
|
387
|
+
try {
|
|
388
|
+
const r = await rpcOnce('notes-read', { file_path: p }, 60_000);
|
|
389
|
+
// `read` returns `cat -n` numbered lines — strip the gutter before writing a real file.
|
|
390
|
+
body = String(r?.content ?? '').replace(/^\s*\d+\t/gm, '');
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (!body.trim())
|
|
396
|
+
continue;
|
|
397
|
+
const target = join(dir, rel);
|
|
398
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
399
|
+
writeFileSync(target, body.endsWith('\n') ? body : `${body}\n`);
|
|
400
|
+
written++;
|
|
401
|
+
}
|
|
402
|
+
console.log(` ${c.mint('✓')} ${written} notes → ${c.bold(dir.replace(root + '/', ''))}`);
|
|
403
|
+
console.log(` ${c.dim('They are plain markdown. Commit them like any other file.')}\n`);
|
|
404
|
+
}
|
|
405
|
+
// ── status ──────────────────────────────────────────────────────────────────────────────────
|
|
406
|
+
async function cmdStatus() {
|
|
407
|
+
const root = repoRoot();
|
|
408
|
+
console.log('');
|
|
409
|
+
console.log(` ${c.bold('clearly')}`);
|
|
410
|
+
console.log(` ${c.dim('─'.repeat(46))}`);
|
|
411
|
+
console.log(` Repo ${root ? root.split('/').pop() : c.dim('not a git repository')}`);
|
|
412
|
+
let notes = 0, reachable = false;
|
|
413
|
+
try {
|
|
414
|
+
const r = await rpcOnce('notes-glob', { pattern: '~/**/*.md', limit: 500 }, 60_000);
|
|
415
|
+
notes = Number(r?.count ?? 0);
|
|
416
|
+
reachable = true;
|
|
417
|
+
}
|
|
418
|
+
catch { /* reported below */ }
|
|
419
|
+
console.log(` Archive ${reachable ? `${notes} notes` : c.red('unreachable')}`);
|
|
420
|
+
console.log(` Sources git ${c.dim('(local commits)')}`);
|
|
421
|
+
/**
|
|
422
|
+
* ⚠ THE UNBUILT SOURCES ARE NAMED HERE ON PURPOSE. The landing page sells GitHub, Linear and
|
|
423
|
+
* Slack; a CLI that stays quiet about not having them lets someone believe a note covered a
|
|
424
|
+
* discussion it never saw. Being the one surface that says what it cannot see is cheap honesty.
|
|
425
|
+
*/
|
|
426
|
+
console.log(` ${c.dim('github, linear, slack — not connected in this build')}`);
|
|
427
|
+
if (!reachable) {
|
|
428
|
+
console.log(`\n ${c.dim('Not connected. Sign in with:')} beehaven login`);
|
|
429
|
+
}
|
|
430
|
+
console.log('');
|
|
431
|
+
}
|
|
432
|
+
// ── login / logout ──────────────────────────────────────────────────────────────────────────
|
|
433
|
+
/**
|
|
434
|
+
* ⚠⚠ AUTH IS DELEGATED, NOT REIMPLEMENTED. `beehaven login` is ~300 lines of loopback OAuth —
|
|
435
|
+
* ephemeral local server, anti-CSRF state, browser round-trip, keychain write — and it is inline
|
|
436
|
+
* in `cli.ts` rather than exported. Copying it here would be a SECOND auth stack against the same
|
|
437
|
+
* accounts, which is how the two drift and how one of them ends up subtly wrong on the day it
|
|
438
|
+
* matters. Both binaries ship from this package, so the sibling is always the right version.
|
|
439
|
+
*
|
|
440
|
+
* The user never sees the delegation: stdio is inherited, so the browser opens and the prompts
|
|
441
|
+
* appear exactly as they would either way.
|
|
442
|
+
*/
|
|
443
|
+
function delegate(verb, args = []) {
|
|
444
|
+
const cli = join(dirname(fileURLToPath(import.meta.url)), 'cli.js');
|
|
445
|
+
const r = spawnSync(process.execPath, [cli, verb, ...args], { stdio: 'inherit' });
|
|
446
|
+
process.exit(r.status ?? 0);
|
|
447
|
+
}
|
|
448
|
+
// ── settings ────────────────────────────────────────────────────────────────────────────────
|
|
449
|
+
async function cmdSettings() {
|
|
450
|
+
const key = argv[1];
|
|
451
|
+
const value = argv[2];
|
|
452
|
+
/**
|
|
453
|
+
* ⚠ ONLY SETTINGS THIS CLI ACTUALLY HONOURS ARE LISTED. A settings screen that shows a key
|
|
454
|
+
* nothing reads is worse than no screen — it invites someone to change a value and conclude the
|
|
455
|
+
* product ignored them.
|
|
456
|
+
*/
|
|
457
|
+
const OWNED = {
|
|
458
|
+
notesDir: 'Where `clearly pull` writes markdown (default: .clearly/notes)',
|
|
459
|
+
};
|
|
460
|
+
if (key && !OWNED[key]) {
|
|
461
|
+
die(`Not a clearly setting: ${key}`, `Settings: ${Object.keys(OWNED).join(', ')}. Account and environment are beehaven's — use \`beehaven env\`.`);
|
|
462
|
+
}
|
|
463
|
+
if (key && value !== undefined) {
|
|
464
|
+
patchCliConfig({ [`clearly.${key}`]: value });
|
|
465
|
+
console.log(`\n ${c.mint('✓')} ${key} = ${value}\n`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const cfg = readCliConfig();
|
|
469
|
+
console.log('');
|
|
470
|
+
console.log(` ${c.bold('settings')}`);
|
|
471
|
+
console.log(` ${c.dim('─'.repeat(52))}`);
|
|
472
|
+
for (const [k, desc] of Object.entries(OWNED)) {
|
|
473
|
+
const v = cfg[`clearly.${k}`] ?? c.dim('(default)');
|
|
474
|
+
console.log(` ${k.padEnd(12)} ${String(v)}`);
|
|
475
|
+
console.log(` ${c.dim(' '.repeat(12) + ' ' + desc)}`);
|
|
476
|
+
}
|
|
477
|
+
console.log('');
|
|
478
|
+
// ⚠ Account and env belong to the daemon and are shown READ-ONLY here, with the verb that owns
|
|
479
|
+
// them — two places that can both write one value is how they disagree.
|
|
480
|
+
console.log(` ${c.dim('Account')} ${String(cfg.email ?? c.dim('not signed in'))}`);
|
|
481
|
+
console.log(` ${c.dim('Environment')} ${String(cfg.envName ?? cfg.env ?? 'prod')} ${c.dim('· change with `beehaven env`')}`);
|
|
482
|
+
console.log('');
|
|
483
|
+
console.log(` ${c.dim('clearly settings <key> <value>')}`);
|
|
484
|
+
console.log('');
|
|
485
|
+
}
|
|
486
|
+
// ── uninstall ───────────────────────────────────────────────────────────────────────────────
|
|
487
|
+
/**
|
|
488
|
+
* ⚠⚠ THIS REMOVES LOCAL STATE. IT DOES NOT TOUCH THE ARCHIVE, AND IT SAYS SO.
|
|
489
|
+
*
|
|
490
|
+
* The notes are the asset — the entire pitch is that cancelling takes nothing away. An uninstall
|
|
491
|
+
* that quietly deleted them, or that left someone UNSURE whether it had, would undo that promise
|
|
492
|
+
* more effectively than any outage. So it names what it deleted and what it did not, every time.
|
|
493
|
+
*
|
|
494
|
+
* ⚠ IT ALSO DOES NOT SIGN YOU OUT unless asked. Auth is shared with `beehaven`, and revoking it
|
|
495
|
+
* here would break a tool the person did not ask about.
|
|
496
|
+
*
|
|
497
|
+
* ⚠⚠ AND IT REMOVES ONLY THE FILES THIS CLI WROTE — NOT `.clearly/`. The first version deleted the
|
|
498
|
+
* whole directory recursively, and running it on this very repo found `README.md` and `REVENUE.md`
|
|
499
|
+
* already living there: `.clearly/` is a SHARED layout (the daemon's sync puts `CLAUDE.md`,
|
|
500
|
+
* `tasks.yaml` and `memories.yaml` in it too). A recursive delete of a directory you shipped one
|
|
501
|
+
* file into is how an uninstall takes someone's documentation with it, silently, and the tell
|
|
502
|
+
* would have been a git diff nobody read until later.
|
|
503
|
+
*/
|
|
504
|
+
const OWNED_PATHS = ['state.json', 'notes'];
|
|
505
|
+
function cmdUninstall() {
|
|
506
|
+
const root = repoRoot() ?? process.cwd();
|
|
507
|
+
const dir = join(root, '.clearly');
|
|
508
|
+
const rel = dir.replace(root + '/', '');
|
|
509
|
+
const mine = OWNED_PATHS.filter((p) => existsSync(join(dir, p)));
|
|
510
|
+
const theirs = (() => {
|
|
511
|
+
try {
|
|
512
|
+
return readdirSync(dir).filter((f) => !OWNED_PATHS.includes(f));
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return [];
|
|
516
|
+
}
|
|
517
|
+
})();
|
|
518
|
+
if (!mine.length) {
|
|
519
|
+
console.log(`\n ${c.dim('Nothing to remove — this repo has no clearly state.')}\n`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (!has('--yes')) {
|
|
523
|
+
console.log(`\n This will remove from ${c.bold(rel)}:`);
|
|
524
|
+
for (const p of mine) {
|
|
525
|
+
console.log(` · ${p} ${c.dim(p === 'notes' ? '(your pulled copy — the archive itself is untouched)' : '(where the last note stopped)')}`);
|
|
526
|
+
}
|
|
527
|
+
if (theirs.length)
|
|
528
|
+
console.log(`\n ${c.dim(`Leaving alone: ${theirs.join(', ')}`)}`);
|
|
529
|
+
console.log(`\n ${c.dim('It does NOT delete your notes archive, and does NOT sign you out.')}`);
|
|
530
|
+
console.log(` ${c.dim('Run again with --yes to confirm.')}\n`);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const gone = [];
|
|
534
|
+
for (const p of mine) {
|
|
535
|
+
try {
|
|
536
|
+
rmSync(join(dir, p), { recursive: true, force: true });
|
|
537
|
+
gone.push(p);
|
|
538
|
+
}
|
|
539
|
+
catch { /* reported */ }
|
|
540
|
+
}
|
|
541
|
+
console.log(`\n ${c.mint('✓')} removed ${gone.join(', ')} from ${rel}`);
|
|
542
|
+
if (theirs.length)
|
|
543
|
+
console.log(` ${c.dim(`Left alone: ${theirs.join(', ')}`)}`);
|
|
544
|
+
console.log(` ${c.dim('Your archive is untouched — `clearly ask` still answers, `clearly pull` restores the files.')}`);
|
|
545
|
+
console.log(` ${c.dim('Still signed in. Sign out with:')} beehaven logout\n`);
|
|
546
|
+
}
|
|
547
|
+
// ── main ────────────────────────────────────────────────────────────────────────────────────
|
|
548
|
+
/**
|
|
549
|
+
* ⚠ THE TWO VERBS THAT DO THE WORK COME FIRST, and the housekeeping is grouped below them. A flat
|
|
550
|
+
* alphabetical list of eight makes `note` and `settings` look equally important, and the first
|
|
551
|
+
* thing a new reader needs is which one to type tonight.
|
|
552
|
+
*/
|
|
553
|
+
function help() {
|
|
554
|
+
console.log(`
|
|
555
|
+
${c.bold('clearly')} ${c.dim('— write the decision down while the context is still warm')}
|
|
556
|
+
|
|
557
|
+
${c.bold('note')} ${c.dim('[--since <ref>] [--dry-run]')}
|
|
558
|
+
Read the work in this repo and write the decision into the archive.
|
|
559
|
+
${c.dim('Defaults to everything since your last note.')}
|
|
560
|
+
${c.dim('--since HEAD~5 that range instead --dry-run show what would be sent')}
|
|
561
|
+
|
|
562
|
+
${c.bold('ask')} ${c.dim('"<question>"')}
|
|
563
|
+
Answer from the archive, citing the note it came from.
|
|
564
|
+
|
|
565
|
+
${c.dim('─── the archive ' + '─'.repeat(38))}
|
|
566
|
+
|
|
567
|
+
${c.bold('pull')} ${c.dim('[--dir <path>]')} Write it into this repo as markdown ${c.dim('(.clearly/notes)')}
|
|
568
|
+
${c.bold('status')} What is connected, and what it can see
|
|
569
|
+
|
|
570
|
+
${c.dim('─── your account ' + '─'.repeat(37))}
|
|
571
|
+
|
|
572
|
+
${c.bold('login')} ${c.dim('·')} ${c.bold('logout')} Sign in or out ${c.dim('(shared with beehaven)')}
|
|
573
|
+
${c.bold('settings')} ${c.dim('[<k> <v>]')} Show or change settings
|
|
574
|
+
${c.bold('uninstall')} ${c.dim('[--yes]')} Remove local state ${c.dim('— never your archive')}
|
|
575
|
+
`);
|
|
576
|
+
}
|
|
577
|
+
(async () => {
|
|
578
|
+
switch (cmd) {
|
|
579
|
+
case 'note':
|
|
580
|
+
await cmdNote();
|
|
581
|
+
break;
|
|
582
|
+
case 'ask':
|
|
583
|
+
await cmdAsk();
|
|
584
|
+
break;
|
|
585
|
+
case 'pull':
|
|
586
|
+
await cmdPull();
|
|
587
|
+
break;
|
|
588
|
+
case 'status':
|
|
589
|
+
await cmdStatus();
|
|
590
|
+
break;
|
|
591
|
+
case 'login':
|
|
592
|
+
delegate('login', argv.slice(1));
|
|
593
|
+
break;
|
|
594
|
+
case 'logout':
|
|
595
|
+
delegate('logout', argv.slice(1));
|
|
596
|
+
break;
|
|
597
|
+
case 'settings':
|
|
598
|
+
await cmdSettings();
|
|
599
|
+
break;
|
|
600
|
+
case 'uninstall':
|
|
601
|
+
cmdUninstall();
|
|
602
|
+
break;
|
|
603
|
+
case 'help':
|
|
604
|
+
case '--help':
|
|
605
|
+
case '-h':
|
|
606
|
+
help();
|
|
607
|
+
break;
|
|
608
|
+
default:
|
|
609
|
+
console.error(`\n ${c.red('✗')} unknown command: ${cmd}`);
|
|
610
|
+
help();
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
})().catch((err) => {
|
|
614
|
+
console.error(`\n ${c.red('✗')} ${err?.message ?? err}\n`);
|
|
615
|
+
process.exit(1);
|
|
616
|
+
});
|