flowviant 0.97.0 → 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -13
- package/bin/cli.mjs +108 -28
- package/bin/lib/agentPlan.mjs +159 -37
- package/bin/lib/artifacts.mjs +136 -26
- package/bin/lib/authReject.mjs +32 -0
- package/bin/lib/authproxy.mjs +52 -16
- package/bin/lib/childEnv.mjs +33 -1
- package/bin/lib/claude.mjs +166 -71
- package/bin/lib/config.mjs +36 -9
- package/bin/lib/credentials.mjs +29 -5
- package/bin/lib/deploy.mjs +364 -30
- package/bin/lib/desktopContract.mjs +227 -0
- package/bin/lib/env.mjs +130 -13
- package/bin/lib/fleet.mjs +121 -30
- package/bin/lib/git.mjs +131 -3
- package/bin/lib/hooks/readGuard.mjs +24 -0
- package/bin/lib/instance.mjs +129 -14
- package/bin/lib/knowledge.mjs +180 -60
- package/bin/lib/landed.mjs +38 -30
- package/bin/lib/launchCommand.mjs +23 -0
- package/bin/lib/listeners.mjs +211 -0
- package/bin/lib/localSessions.mjs +29 -4
- package/bin/lib/login.mjs +36 -12
- package/bin/lib/machines.mjs +24 -14
- package/bin/lib/mcp-cli.mjs +4 -3
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/preview.mjs +95 -6
- package/bin/lib/printable.mjs +42 -0
- package/bin/lib/procRegistry.mjs +65 -0
- package/bin/lib/processes.mjs +20 -7
- package/bin/lib/projectTools.mjs +248 -0
- package/bin/lib/prompts.mjs +16 -11
- package/bin/lib/resources.mjs +46 -4
- package/bin/lib/runtimeOfferings.mjs +75 -0
- package/bin/lib/runtimes.mjs +71 -9
- package/bin/lib/shipMerge.mjs +8 -4
- package/bin/lib/trace.mjs +16 -6
- package/bin/lib/update.mjs +114 -25
- package/bin/lib/work.mjs +1192 -4584
- package/bin/lib/workAgentMerges.mjs +504 -0
- package/bin/lib/workAgentPlans.mjs +447 -0
- package/bin/lib/workAgentReview.mjs +702 -0
- package/bin/lib/workAgentTurns.mjs +1151 -0
- package/bin/lib/workDiffs.mjs +107 -0
- package/bin/lib/workPreviews.mjs +351 -0
- package/bin/lib/workProcesses.mjs +236 -0
- package/bin/lib/workPullRequests.mjs +313 -0
- package/bin/lib/workShip.mjs +385 -0
- package/bin/lib/worktreeDiff.mjs +99 -43
- package/package.json +2 -3
package/bin/lib/claude.mjs
CHANGED
|
@@ -61,10 +61,66 @@ export const READ_GUARD_SETTINGS = JSON.stringify({
|
|
|
61
61
|
* `--settings` pair must stand before it or be swallowed as a tool name. */
|
|
62
62
|
const READ_GUARD = ['--settings', READ_GUARD_SETTINGS];
|
|
63
63
|
|
|
64
|
+
/** A path a permission rule can name: absolute POSIX, and free of the glob and
|
|
65
|
+
* rule-syntax characters that would change what the rule means. Anything else
|
|
66
|
+
* gets no rule — `--add-dir` still admits the directory (measured). */
|
|
67
|
+
const ruleSafeAbsolute = (p) =>
|
|
68
|
+
typeof p === 'string' && p.startsWith('/') && !/[*?[\]{}()\\\n]/.test(p) ? p.replace(/\/+$/, '') : null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* THE READ FENCE — the worktree, plus the knowledge library by its absolute
|
|
72
|
+
* path, and nothing else on the box (2026-09-24, the audit).
|
|
73
|
+
*
|
|
74
|
+
* Every curated posture used to allow bare `Read`, `Grep` and `Glob`, and a
|
|
75
|
+
* bare allow is not "read the repo": measured on Claude Code 2.1.281, bare
|
|
76
|
+
* `Read` read a file outside the working directory and bare `Grep` searched
|
|
77
|
+
* one. So a design, consult or capture turn — each steered by words anyone who
|
|
78
|
+
* can file a card can write — could read `~/.flowviant/credentials.json`, which
|
|
79
|
+
* holds the MACHINE CREDENTIAL of every project connected on this box, and put
|
|
80
|
+
* it in an artifact, a plan note or a transcript that leaves the machine. That
|
|
81
|
+
* is a cross-project leak from a posture this file calls read-only.
|
|
82
|
+
*
|
|
83
|
+
* The fence is research's, measured the same day in the design shape:
|
|
84
|
+
* `Read(./**)` and `Glob(./**)` read the worktree; the knowledge library reads
|
|
85
|
+
* through `--add-dir` plus its own `//` rule; a file outside both was refused
|
|
86
|
+
* ("Claude requested permissions to read from …, but you haven't granted it
|
|
87
|
+
* yet"). GREP CARRIES NO ALLOW RULE AT ALL, on purpose: with none, Grep in the
|
|
88
|
+
* working directory (and in the added knowledge directory) ran, and Grep of a
|
|
89
|
+
* path outside them was refused. The Bash readers these postures keep are
|
|
90
|
+
* path-validated by the CLI on its own (see CONSULT_BASH).
|
|
91
|
+
*/
|
|
92
|
+
function fencedReads(knowledgeDir) {
|
|
93
|
+
const kd = ruleSafeAbsolute(knowledgeDir);
|
|
94
|
+
return [
|
|
95
|
+
'Read(./**)',
|
|
96
|
+
...(kd ? [`Read(/${kd}/**)`] : []),
|
|
97
|
+
'Glob(./**)',
|
|
98
|
+
...(kd ? [`Glob(/${kd}/**)`] : []),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The shell readers CONSULT and PLAN keep. Each one is PATH-VALIDATED by the
|
|
103
|
+
* CLI itself — measured on 2.1.281 (2026-09-24): with `Bash(cat:*)`,
|
|
104
|
+
* `Bash(head:*)` and `Bash(ls:*)` allowed, `cat`/`ls`/`head` on a file outside
|
|
105
|
+
* the working directories were each refused ("Claude Code may only
|
|
106
|
+
* concatenate files from the allowed working directories") — so it is the
|
|
107
|
+
* Read/Grep/Glob TOOLS, not these, that needed the fence above. */
|
|
108
|
+
const CONSULT_BASH = [
|
|
109
|
+
'Bash(ls:*)',
|
|
110
|
+
'Bash(wc:*)',
|
|
111
|
+
'Bash(head:*)',
|
|
112
|
+
'Bash(cat:*)',
|
|
113
|
+
'Bash(git log:*)',
|
|
114
|
+
'Bash(git show:*)',
|
|
115
|
+
'Bash(git diff:*)',
|
|
116
|
+
'Bash(git rev-parse:*)',
|
|
117
|
+
];
|
|
118
|
+
|
|
64
119
|
/**
|
|
65
120
|
* PLAN — read the repo, write the plan, never the code.
|
|
66
121
|
*
|
|
67
|
-
* The read half is
|
|
122
|
+
* The read half is `consultPermFor`'s verbatim (the read fence and the
|
|
123
|
+
* path-validated shell readers): this turn's prompt is steered by
|
|
68
124
|
* anything a project editor can type, so the same threat applies and the same
|
|
69
125
|
* allowlist answers it. What is added is the control plane and NOTHING else —
|
|
70
126
|
* `mcp__flowviant` is the plan principal's token, whose entire tool set is the
|
|
@@ -77,22 +133,15 @@ const READ_GUARD = ['--settings', READ_GUARD_SETTINGS];
|
|
|
77
133
|
* through an API, and there is no file on this machine it has any business
|
|
78
134
|
* touching.
|
|
79
135
|
*/
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
'Bash(head:*)',
|
|
90
|
-
'Bash(cat:*)',
|
|
91
|
-
'Bash(git log:*)',
|
|
92
|
-
'Bash(git show:*)',
|
|
93
|
-
'Bash(git diff:*)',
|
|
94
|
-
'Bash(git rev-parse:*)',
|
|
95
|
-
];
|
|
136
|
+
export function planPermFor(knowledgeDir) {
|
|
137
|
+
return [
|
|
138
|
+
...READ_GUARD,
|
|
139
|
+
'--allowedTools',
|
|
140
|
+
'mcp__flowviant',
|
|
141
|
+
...fencedReads(knowledgeDir),
|
|
142
|
+
...CONSULT_BASH,
|
|
143
|
+
];
|
|
144
|
+
}
|
|
96
145
|
|
|
97
146
|
// Unattended (default) skips prompts so the agent never stalls with no terminal;
|
|
98
147
|
// FLOWVIANT_SAFE=1 restricts to a curated toolset instead.
|
|
@@ -154,21 +203,9 @@ const WIKI_PERM = [
|
|
|
154
203
|
// someone else's machine. The permission list is the enforcement; the prompt's
|
|
155
204
|
// "do not change anything" is only an instruction, and instructions are exactly
|
|
156
205
|
// what an injected question competes with.
|
|
157
|
-
|
|
158
|
-
...READ_GUARD,
|
|
159
|
-
|
|
160
|
-
'Read',
|
|
161
|
-
'Grep',
|
|
162
|
-
'Glob',
|
|
163
|
-
'Bash(ls:*)',
|
|
164
|
-
'Bash(wc:*)',
|
|
165
|
-
'Bash(head:*)',
|
|
166
|
-
'Bash(cat:*)',
|
|
167
|
-
'Bash(git log:*)',
|
|
168
|
-
'Bash(git show:*)',
|
|
169
|
-
'Bash(git diff:*)',
|
|
170
|
-
'Bash(git rev-parse:*)',
|
|
171
|
-
];
|
|
206
|
+
export function consultPermFor(knowledgeDir) {
|
|
207
|
+
return [...READ_GUARD, '--allowedTools', ...fencedReads(knowledgeDir), ...CONSULT_BASH];
|
|
208
|
+
}
|
|
172
209
|
|
|
173
210
|
/**
|
|
174
211
|
* THE TWO NON-CODE POSTURES (0.97.0) — a design card and a research card may
|
|
@@ -195,8 +232,10 @@ const CONSULT_PERM = [
|
|
|
195
232
|
*
|
|
196
233
|
* THEY ARE NOT THE SAME READER, and the difference is the network:
|
|
197
234
|
*
|
|
198
|
-
* · DESIGN draws THIS product, so it reads the repo
|
|
199
|
-
*
|
|
235
|
+
* · DESIGN draws THIS product, so it reads the repo — the worktree and the
|
|
236
|
+
* knowledge library through the read fence (`fencedReads`, 2026-09-24:
|
|
237
|
+
* it was bare Read/Grep/Glob, which reach the whole box), Grep with no
|
|
238
|
+
* allow rule, `ls`, and `.env*` denied by name — and has no web at all (a mockup that went looking on the
|
|
200
239
|
* web would be a mockup of somebody else's product). No git readers and no
|
|
201
240
|
* cat/head/wc: Read covers every one of them, and each git reader was an
|
|
202
241
|
* `--output` away from a write (see READ_GUARD, which it still carries for
|
|
@@ -233,9 +272,10 @@ const CONSULT_PERM = [
|
|
|
233
272
|
* STATED, not closed: research may still read any file in the worktree and
|
|
234
273
|
* name it in a WebFetch URL — the repository is what a research card is about,
|
|
235
274
|
* and a turn that could not read it could not answer. Design may read any
|
|
236
|
-
* file
|
|
237
|
-
*
|
|
238
|
-
*
|
|
275
|
+
* file in its worktree and put it in an artifact the server stores; text
|
|
276
|
+
* artifacts are scrubbed of the machine's known secret values — and of any
|
|
277
|
+
* Flowviant credential, by shape — on the way out (artifacts.mjs, env.mjs), and
|
|
278
|
+
* binary ones are withheld when they carry one.
|
|
239
279
|
* Design still runs a plain `git log` on the CLI's own read-only classifier
|
|
240
280
|
* though no git reader is allowed (measured) — the guard is what refuses its
|
|
241
281
|
* `--output`. And the Agent tool is offered with no allow rule on this CLI:
|
|
@@ -274,21 +314,22 @@ const ARTIFACT_WRITE = 'Edit(.flowviant/artifacts/**)';
|
|
|
274
314
|
* plane whose every call the CLI refuses. See work.mjs.
|
|
275
315
|
*/
|
|
276
316
|
export const PLAN_MODE_PERM = ['--permission-mode', 'plan'];
|
|
277
|
-
export
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
317
|
+
export function designPermFor(knowledgeDir) {
|
|
318
|
+
return [
|
|
319
|
+
...READ_GUARD,
|
|
320
|
+
'--allowedTools',
|
|
321
|
+
...fencedReads(knowledgeDir),
|
|
322
|
+
'Bash(ls:*)',
|
|
323
|
+
ARTIFACT_WRITE,
|
|
324
|
+
// A mockup is uploaded and runs scripts in a frame that may navigate
|
|
325
|
+
// itself, so the checkout's own secrets are denied by name too — the same
|
|
326
|
+
// pair research carries, measured load-bearing there.
|
|
327
|
+
'--disallowedTools',
|
|
328
|
+
'Read(./.env*)',
|
|
329
|
+
'Read(./**/.env*)',
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
export const DESIGN_PERM = designPermFor(null);
|
|
292
333
|
|
|
293
334
|
/**
|
|
294
335
|
* RESEARCH, built at spawn — the knowledge library is the one directory
|
|
@@ -296,14 +337,10 @@ const ruleSafeAbsolute = (p) =>
|
|
|
296
337
|
* `//` is the CLI's own spelling for an absolute path in a rule.
|
|
297
338
|
*/
|
|
298
339
|
export function researchPerm(knowledgeDir) {
|
|
299
|
-
const kd = ruleSafeAbsolute(knowledgeDir);
|
|
300
340
|
return [
|
|
301
341
|
...READ_GUARD,
|
|
302
342
|
'--allowedTools',
|
|
303
|
-
|
|
304
|
-
...(kd ? [`Read(/${kd}/**)`] : []),
|
|
305
|
-
'Glob(./**)',
|
|
306
|
-
...(kd ? [`Glob(/${kd}/**)`] : []),
|
|
343
|
+
...fencedReads(knowledgeDir),
|
|
307
344
|
'WebSearch',
|
|
308
345
|
'WebFetch',
|
|
309
346
|
ARTIFACT_WRITE,
|
|
@@ -322,10 +359,26 @@ export const sleep = (s) => new Promise((r) => setTimeout(r, s * 1000));
|
|
|
322
359
|
// matching falsely fired when an agent merely *mentioned* the word in prose
|
|
323
360
|
// (e.g. "I won't fabricate a BLOCKED:<id> line"), trapping the worker in a fake
|
|
324
361
|
// blocked loop. Anchor to a full line instead.
|
|
325
|
-
|
|
362
|
+
//
|
|
363
|
+
// LINE-SPLIT, NEVER A `^...$` REGEX WITH A `\s*` ON BOTH ENDS (audit
|
|
364
|
+
// 2026-09-24). `new RegExp('^\\s*NAME\\s*$', 'm')` and
|
|
365
|
+
// `/^\s*BLOCKED:(\S+)\s*$/m` are QUADRATIC against a long run of
|
|
366
|
+
// whitespace on one line — the same shape `taskIdsFromMessage` already had
|
|
367
|
+
// to fix, measured ~0.5s at 40KB here — and a CLI's own stdout is exactly
|
|
368
|
+
// the kind of text a stray control sequence or a pasted blob can grow past
|
|
369
|
+
// that. A length cap first, then plain string work, same as there.
|
|
370
|
+
const SENTINEL_MAX_LINE = 2000;
|
|
371
|
+
export const sawSentinel = (out, name) =>
|
|
372
|
+
String(out ?? '')
|
|
373
|
+
.split('\n')
|
|
374
|
+
.some((l) => l.length < SENTINEL_MAX_LINE && l.trim() === name);
|
|
326
375
|
export const blockedId = (out) => {
|
|
327
|
-
const
|
|
328
|
-
|
|
376
|
+
for (const l of String(out ?? '').split('\n')) {
|
|
377
|
+
if (l.length > SENTINEL_MAX_LINE) continue;
|
|
378
|
+
const t = l.trim();
|
|
379
|
+
if (t.startsWith('BLOCKED:') && /^BLOCKED:\S+$/.test(t)) return t.slice(8);
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
329
382
|
};
|
|
330
383
|
|
|
331
384
|
/**
|
|
@@ -539,6 +592,27 @@ export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, app
|
|
|
539
592
|
}
|
|
540
593
|
}
|
|
541
594
|
|
|
595
|
+
/**
|
|
596
|
+
* THE ENVIRONMENT A CLI TURN IS SPAWNED WITH — the daemon's own, plus the
|
|
597
|
+
* turn's MCP token, MINUS the machine credential (2026-09-24, the audit).
|
|
598
|
+
*
|
|
599
|
+
* A turn cannot be handed a curated environment the way `childEnv` builds one
|
|
600
|
+
* for a deploy: the CLI's own sign-in lives in this environment, and stripping
|
|
601
|
+
* it signs the CLI out. But on a headless box started as
|
|
602
|
+
* `FLOWVIANT_MACHINE_TOKEN=… npx flowviant` the MACHINE CREDENTIAL lives here
|
|
603
|
+
* too, and every turn — and every dev server and script a turn starts, which
|
|
604
|
+
* inherit it — could read it with one `env`. Nothing a CLI or its children do
|
|
605
|
+
* needs it: the daemon authenticates to the server itself, and a turn that
|
|
606
|
+
* needs the project's tools gets its own per-turn token through `mcpEnv` or
|
|
607
|
+
* the MCP config file. So those two names, and only those, are removed.
|
|
608
|
+
*/
|
|
609
|
+
const MACHINE_CREDENTIAL_ENV = ['FLOWVIANT_MACHINE_TOKEN', 'FLOWVIANT_FLEET'];
|
|
610
|
+
export function cliEnv(mcpEnv) {
|
|
611
|
+
const env = { ...process.env, ...(mcpEnv ?? {}) };
|
|
612
|
+
for (const k of MACHINE_CREDENTIAL_ENV) delete env[k];
|
|
613
|
+
return env;
|
|
614
|
+
}
|
|
615
|
+
|
|
542
616
|
// One Claude Code turn. Output is captured (for sentinel detection) and streamed
|
|
543
617
|
// through, line-prefixed with the worker label so a fleet stays legible.
|
|
544
618
|
//
|
|
@@ -548,7 +622,7 @@ export function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, app
|
|
|
548
622
|
// returned string for sentinel detection, and each activity is handed to
|
|
549
623
|
// `onActivity` so the caller can forward progress. Build-agent turns leave it
|
|
550
624
|
// off and keep the raw text passthrough + line sentinels.
|
|
551
|
-
export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onToolEvent, onInit, onUsage, onThreadId, wikiPerm, readOnly, planPerm, planMode, posture, vaultDir, knowledgeDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId }) {
|
|
625
|
+
export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEnv, agentTools, runtime = 'claude', label, onSpawn, streamJson, answerFromResult, onActivity, onToolEvent, onInit, onUsage, onThreadId, onAnswer, wikiPerm, readOnly, planPerm, planMode, posture, vaultDir, knowledgeDir, resultSchemaArgs, model, effort, adoptResumeId, resumeThreadId, resumeConversationId }) {
|
|
552
626
|
return new Promise((resolve) => {
|
|
553
627
|
const rt = runtimeById(runtime);
|
|
554
628
|
// PLAN MODE IS CLAUDE'S (0.97.0). The other adapters build their argv
|
|
@@ -622,6 +696,7 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
|
|
|
622
696
|
resume,
|
|
623
697
|
streamJson,
|
|
624
698
|
profile,
|
|
699
|
+
agentTools,
|
|
625
700
|
// Adopting a terminal session (work.mjs): Claude turns it into
|
|
626
701
|
// `--resume <id> --fork-session` (a FORK — the original is untouched);
|
|
627
702
|
// agy turns it into `--conversation <id>` (a MOVE — agy has no fork, the
|
|
@@ -652,15 +727,19 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
|
|
|
652
727
|
perm: planMode
|
|
653
728
|
? PLAN_MODE_PERM
|
|
654
729
|
: profile === 'design'
|
|
655
|
-
?
|
|
730
|
+
? designPermFor(knowledgeDir)
|
|
656
731
|
: profile === 'research'
|
|
657
732
|
? researchPerm(knowledgeDir)
|
|
658
|
-
: planPerm
|
|
733
|
+
: planPerm
|
|
734
|
+
? planPermFor(knowledgeDir)
|
|
735
|
+
: readOnly
|
|
736
|
+
? consultPermFor(knowledgeDir)
|
|
737
|
+
: wikiPerm ? WIKI_PERM : PERM,
|
|
659
738
|
// Handed to the adapter rather than appended here, because WHERE these go
|
|
660
739
|
// is a property of the CLI: Codex reads its prompt as a trailing
|
|
661
740
|
// positional, so a flag after it is a flag in the wrong place.
|
|
662
741
|
// Wiki-vault turns are pure file work and pass neither — no MCP at all.
|
|
663
|
-
mcp: mcpConfig ? ['--mcp-config', mcpConfig] : (mcpArgs ?? []),
|
|
742
|
+
mcp: planMode ? ['--strict-mcp-config'] : mcpConfig ? ['--mcp-config', mcpConfig] : (mcpArgs ?? []),
|
|
664
743
|
// Resuming a SPECIFIC held conversation by its own id (work.mjs, codex
|
|
665
744
|
// sessions). Runtimes without a by-id resume ignore it and keep their
|
|
666
745
|
// `resume` behavior unchanged.
|
|
@@ -706,8 +785,9 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
|
|
|
706
785
|
detached: true,
|
|
707
786
|
// Only ADDS to the environment (the worker token, for runtimes that read
|
|
708
787
|
// it from there). Never replaces it: the CLI's own credentials live in
|
|
709
|
-
// this environment, and handing it a curated one signs it out.
|
|
710
|
-
|
|
788
|
+
// this environment, and handing it a curated one signs it out. The one
|
|
789
|
+
// thing it REMOVES is this daemon's machine credential — see `cliEnv`.
|
|
790
|
+
env: cliEnv(mcpEnv),
|
|
711
791
|
});
|
|
712
792
|
onSpawn?.(child);
|
|
713
793
|
let out = '';
|
|
@@ -734,14 +814,28 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
|
|
|
734
814
|
// thread.started). Purely additive: callers that pass no onThreadId —
|
|
735
815
|
// every dispatch path — see zero behavior change.
|
|
736
816
|
if (ev.threadId) onThreadId?.(ev.threadId);
|
|
817
|
+
// The runtime's own token count (codex's `turn.completed`) — the same
|
|
818
|
+
// `onUsage` Claude's result event feeds, so a caller charges a turn
|
|
819
|
+
// identically whichever CLI ran it.
|
|
820
|
+
if (ev.usage) onUsage?.(ev.usage);
|
|
821
|
+
// One agent MESSAGE, alone — see parseCodexLine's `answer`. Last call
|
|
822
|
+
// wins at the caller, which is what "the final answer" means.
|
|
823
|
+
if (typeof ev.answer === 'string') onAnswer?.(ev.answer);
|
|
737
824
|
if (ev.text) appendText(ev.text);
|
|
738
825
|
if (ev.activity) {
|
|
739
826
|
emit(`${ev.activity.label}\n`);
|
|
740
827
|
onActivity?.(ev.activity);
|
|
741
828
|
}
|
|
742
829
|
};
|
|
830
|
+
// DECODED AS A STREAM, never chunk by chunk (2026-09-24, the audit): a
|
|
831
|
+
// pipe read can end inside a multi-byte character — the em dash models
|
|
832
|
+
// write constantly — and `d.toString()` per chunk turned each half into
|
|
833
|
+
// U+FFFD, which JSON.parse accepts and the reply then carried verbatim.
|
|
834
|
+
// `setEncoding` holds the partial bytes over to the next chunk.
|
|
835
|
+
child.stdout.setEncoding('utf8');
|
|
836
|
+
child.stderr.setEncoding('utf8');
|
|
743
837
|
child.stdout.on('data', (d) => {
|
|
744
|
-
buf += d
|
|
838
|
+
buf += d;
|
|
745
839
|
let nl;
|
|
746
840
|
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
747
841
|
const line = buf.slice(0, nl);
|
|
@@ -750,8 +844,7 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
|
|
|
750
844
|
}
|
|
751
845
|
});
|
|
752
846
|
// stderr is not JSON (warnings/errors) — pass through and keep for sentinels.
|
|
753
|
-
child.stderr.on('data', (
|
|
754
|
-
const s = d.toString();
|
|
847
|
+
child.stderr.on('data', (s) => {
|
|
755
848
|
out += s;
|
|
756
849
|
emit(s);
|
|
757
850
|
});
|
|
@@ -780,11 +873,13 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, mcpArgs, mcpEn
|
|
|
780
873
|
return;
|
|
781
874
|
}
|
|
782
875
|
|
|
783
|
-
const onChunk = (
|
|
784
|
-
const s = d.toString();
|
|
876
|
+
const onChunk = (s) => {
|
|
785
877
|
out += s;
|
|
786
878
|
emit(s);
|
|
787
879
|
};
|
|
880
|
+
// Stream-decoded for the same reason as the line-parsed path above.
|
|
881
|
+
child.stdout.setEncoding('utf8');
|
|
882
|
+
child.stderr.setEncoding('utf8');
|
|
788
883
|
child.stdout.on('data', onChunk);
|
|
789
884
|
child.stderr.on('data', onChunk);
|
|
790
885
|
child.on('error', (e) => {
|
package/bin/lib/config.mjs
CHANGED
|
@@ -11,14 +11,18 @@ import { cpus, hostname, totalmem } from 'node:os';
|
|
|
11
11
|
// '0.28.0' across every release through 0.28.6, so the startup banner, the
|
|
12
12
|
// User-Agent the server version-gates on, and the self-update check all reported
|
|
13
13
|
// a stale version (and the "update available" nag never cleared).
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
// The compile script substitutes this constant into the binary. npm installs
|
|
15
|
+
// still read their own package.json exactly as before (including on Node 20).
|
|
16
|
+
export const VERSION = typeof __FLOWVIANT_BUILD_VERSION__ === 'string'
|
|
17
|
+
? __FLOWVIANT_BUILD_VERSION__
|
|
18
|
+
: (() => {
|
|
19
|
+
try {
|
|
20
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'package.json');
|
|
21
|
+
return JSON.parse(readFileSync(pkgPath, 'utf8')).version || '0.0.0';
|
|
22
|
+
} catch {
|
|
23
|
+
return '0.0.0';
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
22
26
|
|
|
23
27
|
// The model EVERY daemon Claude turn runs on — pinned so autonomous work never
|
|
24
28
|
// inherits your interactive `~/.claude/settings.json` default. That matters: a
|
|
@@ -327,7 +331,30 @@ export let FLEET_TOKEN =
|
|
|
327
331
|
* A frozen copy read at import would salt a picked project's report with null
|
|
328
332
|
* and report nothing at all, for the whole life of the process.
|
|
329
333
|
*/
|
|
330
|
-
export let PROJECT_ID =
|
|
334
|
+
export let PROJECT_ID =
|
|
335
|
+
CREDENTIAL.entry?.fleetToken && CREDENTIAL.entry.fleetToken === FLEET_TOKEN
|
|
336
|
+
? (CREDENTIAL.entry.projectId ?? null)
|
|
337
|
+
: null;
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* THE STORE'S PROJECT IS THIS DAEMON'S ONLY WHEN IT IS THIS DAEMON'S TOKEN.
|
|
341
|
+
* `FLEET_TOKEN` prefers `--fleet` and the environment over the store, and the
|
|
342
|
+
* id above used to come from the store unconditionally — so a box holding one
|
|
343
|
+
* unbound project A, started with project B's token in the environment, salted
|
|
344
|
+
* B's env report with A's id (every variable then read `differs` beside B's
|
|
345
|
+
* other boxes, the confident wrong answer the salt exists to prevent) and
|
|
346
|
+
* printed "serves · A". It starts null in that case, and the ROSTER's own word
|
|
347
|
+
* settles it here on the first poll — the "names no project until the roster
|
|
348
|
+
* does" the paragraph above always promised and nothing ever did.
|
|
349
|
+
*/
|
|
350
|
+
export function learnProjectId(id) {
|
|
351
|
+
if (typeof id === 'string' && id) PROJECT_ID = id;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Whether the stored resolution describes the token actually in use. */
|
|
355
|
+
export function storedCredentialInUse() {
|
|
356
|
+
return Boolean(CREDENTIAL.entry?.fleetToken) && CREDENTIAL.entry.fleetToken === FLEET_TOKEN;
|
|
357
|
+
}
|
|
331
358
|
|
|
332
359
|
/** cli.mjs's picker chose. Must run BEFORE runFleetDaemon — nothing here
|
|
333
360
|
* re-authenticates a connection already made. */
|
package/bin/lib/credentials.mjs
CHANGED
|
@@ -48,6 +48,8 @@ import { execFileSync } from 'node:child_process';
|
|
|
48
48
|
import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
49
49
|
import { homedir } from 'node:os';
|
|
50
50
|
import { join } from 'node:path';
|
|
51
|
+
import { printable } from './printable.mjs';
|
|
52
|
+
import { terminalCommand } from './launchCommand.mjs';
|
|
51
53
|
|
|
52
54
|
const CRED_DIR = join(homedir(), '.flowviant');
|
|
53
55
|
const CRED_FILE = join(CRED_DIR, 'credentials.json');
|
|
@@ -133,7 +135,28 @@ export function listStoredProjects() {
|
|
|
133
135
|
|
|
134
136
|
/** What a project is CALLED on a terminal: its name, or an id you can grep. */
|
|
135
137
|
export function projectLabel(e) {
|
|
136
|
-
return e?.name ?? (e?.projectId ? `project ${e.projectId.slice(0, 8)}…` : 'an unnamed project');
|
|
138
|
+
return safeName(e?.name) ?? (e?.projectId ? `project ${e.projectId.slice(0, 8)}…` : 'an unnamed project');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* A SERVER-SUPPLIED NAME, SAFE TO PRINT ON A TERMINAL — or null.
|
|
143
|
+
*
|
|
144
|
+
* A project's name is whatever an owner typed in the app, and the server's
|
|
145
|
+
* schema caps its length and nothing else. Printed raw it reaches every
|
|
146
|
+
* terminal that connects a machine to the project — `login`, the banner, the
|
|
147
|
+
* picker, `projects`, `machines` — and a name carrying an escape sequence can
|
|
148
|
+
* rewrite the clipboard (OSC 52 in kitty, WezTerm, Windows Terminal) or hide
|
|
149
|
+
* lines of the very listing whose job is to say which project is which. The
|
|
150
|
+
* scrub is `printable()` (C0, DEL, C1 — the server's own `parseBoxName` makes
|
|
151
|
+
* the same cut for hostnames — plus the bidi/format controls that reorder a
|
|
152
|
+
* line, added 2026-09-24 rather than re-derived: see printable.mjs), and what
|
|
153
|
+
* is left is trimmed; nothing left is null, so the caller's id fallback
|
|
154
|
+
* speaks instead of an empty label.
|
|
155
|
+
*/
|
|
156
|
+
export function safeName(name) {
|
|
157
|
+
if (typeof name !== 'string') return null;
|
|
158
|
+
const clean = printable(name).trim();
|
|
159
|
+
return clean || null;
|
|
137
160
|
}
|
|
138
161
|
|
|
139
162
|
/**
|
|
@@ -259,7 +282,7 @@ export function directoryTakenRefusal(bound, repoRoot, { incoming } = {}) {
|
|
|
259
282
|
`${head}\n` +
|
|
260
283
|
'A directory serves one project — two projects must not edit it at the same time.\n' +
|
|
261
284
|
'Delete the project you do not mean in Flowviant first (project settings → General → Delete project),\n' +
|
|
262
|
-
|
|
285
|
+
`or disconnect this box from it with \`${terminalCommand('machines')}\`, then run this again.`
|
|
263
286
|
);
|
|
264
287
|
}
|
|
265
288
|
|
|
@@ -316,7 +339,7 @@ export function saveLogin({ fleetToken, projectId, mcpUrl, name, repoRoot }) {
|
|
|
316
339
|
...(f.projects[projectId] ?? {}),
|
|
317
340
|
fleetToken,
|
|
318
341
|
mcpUrl: mcpUrl ?? null,
|
|
319
|
-
...(name ? { name } : {}),
|
|
342
|
+
...(safeName(name) ? { name: safeName(name) } : {}),
|
|
320
343
|
...(repoRoot ? { repoRoot } : {}),
|
|
321
344
|
savedAt: new Date().toISOString(),
|
|
322
345
|
};
|
|
@@ -391,8 +414,9 @@ export function bindStoredRepo(projectId, repoRoot) {
|
|
|
391
414
|
|
|
392
415
|
/** The roster names the project on every poll; remember it so the picker and
|
|
393
416
|
* `flowviant projects` can say a NAME instead of an id. */
|
|
394
|
-
export function setStoredProjectName(projectId,
|
|
395
|
-
|
|
417
|
+
export function setStoredProjectName(projectId, raw) {
|
|
418
|
+
const name = safeName(raw);
|
|
419
|
+
if (!projectId || !name) return;
|
|
396
420
|
const entries = listStoredProjects();
|
|
397
421
|
const e = entries.find((x) => x.projectId === projectId);
|
|
398
422
|
if (!e || e.name === name) return;
|