flowviant 0.103.0 → 0.104.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/bin/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +93 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactPolicy.mjs +44 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +47 -227
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +367 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +183 -2383
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +2 -2
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +487 -0
- package/bin/lib/knowledgeLibrary.mjs +249 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +28 -42
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +17 -17
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3034
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +207 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +91 -898
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- package/bin/lib/patch.mjs +0 -363
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVERY TERMINAL COMMAND, ONCE (2026-09-26, SOLID F054).
|
|
3
|
+
*
|
|
4
|
+
* A command used to live in three inventories: help.mjs's table, cli.mjs's
|
|
5
|
+
* `noCheckoutCommand` set (whether `--dir` moves the process into the
|
|
6
|
+
* checkout before config.mjs reads the project bound to cwd), and cli.mjs's
|
|
7
|
+
* chain of `process.argv[2] === '<name>'` conditionals — and help.test.mjs
|
|
8
|
+
* only kept two of them aligned. This table is the one list: help.mjs renders
|
|
9
|
+
* it, cli.mjs asks `commandUsesCheckout` for the --dir policy and dispatches
|
|
10
|
+
* by `findCommand(...).name` to a named handler in its HANDLERS map.
|
|
11
|
+
* help.test.mjs pins that every row has exactly one handler (or is one of the
|
|
12
|
+
* BUILTIN rows cli.mjs answers before dispatch).
|
|
13
|
+
*
|
|
14
|
+
* NO IMPORTS, on purpose: cli.mjs reads this before `--dir` is applied, which
|
|
15
|
+
* is before config.mjs may load.
|
|
16
|
+
*
|
|
17
|
+
* `hidden` rows are real commands a person rarely types (an agent calls
|
|
18
|
+
* `shot`; `mcp` manages connectors from a script); they answer
|
|
19
|
+
* `help <name>` but stay off the main screen. `usesCheckout` is true where
|
|
20
|
+
* the command works on the checkout `--dir` names.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const COMMANDS = [
|
|
24
|
+
{
|
|
25
|
+
name: 'start',
|
|
26
|
+
usesCheckout: true,
|
|
27
|
+
usage: 'flowviant [start] [--project <name|id>]',
|
|
28
|
+
summary: "Serve this repo's project: run agents and Workbench tabs here. Plain `flowviant` does the same.",
|
|
29
|
+
details: [
|
|
30
|
+
'Run it inside the repository you connected. The first run in a new repo asks you to approve this computer.',
|
|
31
|
+
'--project <name|id> serve that stored project instead of the one this repo is bound to',
|
|
32
|
+
],
|
|
33
|
+
group: 'run',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'login',
|
|
37
|
+
usesCheckout: true,
|
|
38
|
+
usage: 'flowviant login [--no-start]',
|
|
39
|
+
summary: 'Connect this repo to a project: approve the code in your browser, then start serving.',
|
|
40
|
+
details: ['--no-start save the connection without starting the daemon'],
|
|
41
|
+
group: 'run',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'stop',
|
|
45
|
+
usesCheckout: false,
|
|
46
|
+
usage: 'flowviant stop [--project <name|id>]',
|
|
47
|
+
summary: 'Stop every flowviant daemon on this computer (or just one project’s).',
|
|
48
|
+
group: 'run',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'status',
|
|
52
|
+
usesCheckout: false,
|
|
53
|
+
usage: 'flowviant status',
|
|
54
|
+
summary: 'Each project connected here: its repo, whether it is running and serving, and its live agents.',
|
|
55
|
+
details: ['--json the machine-readable form the Windows tray reads'],
|
|
56
|
+
group: 'look',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'logs',
|
|
60
|
+
usesCheckout: true,
|
|
61
|
+
usage: 'flowviant logs [-f] [--project <name|id>]',
|
|
62
|
+
summary: "The daemon's log for this repo's project.",
|
|
63
|
+
details: ['-f, --follow keep printing new lines as they are written', '-n <lines> how many lines to show first (default 80)'],
|
|
64
|
+
group: 'look',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'open',
|
|
68
|
+
usesCheckout: true,
|
|
69
|
+
usage: 'flowviant open [--project <name|id>]',
|
|
70
|
+
summary: "Open this project's board in your browser (prints the link too).",
|
|
71
|
+
group: 'look',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'projects',
|
|
75
|
+
usesCheckout: false,
|
|
76
|
+
usage: 'flowviant projects',
|
|
77
|
+
summary: 'Every project this computer has a login for, and the repo each one serves.',
|
|
78
|
+
group: 'look',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'machines',
|
|
82
|
+
usesCheckout: false,
|
|
83
|
+
usage: 'flowviant machines [--remove <id> | --forget <id>] [--yes]',
|
|
84
|
+
summary: 'Every computer that serves your projects. On a terminal, a menu to disconnect or forget one here.',
|
|
85
|
+
group: 'look',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'doctor',
|
|
89
|
+
usesCheckout: true,
|
|
90
|
+
usage: 'flowviant doctor',
|
|
91
|
+
summary: 'Check what this computer needs: git, a signed-in CLI, a connection to Flowviant, a running daemon.',
|
|
92
|
+
group: 'look',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'update',
|
|
96
|
+
usesCheckout: false,
|
|
97
|
+
usage: 'flowviant update',
|
|
98
|
+
summary: 'Install the newest flowviant now. It also updates itself when idle.',
|
|
99
|
+
group: 'manage',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'clean',
|
|
103
|
+
usesCheckout: false,
|
|
104
|
+
usage: 'flowviant clean',
|
|
105
|
+
summary: 'Delete the kept agent worktrees under ~/.flowviant/worktrees. Stop the daemon first.',
|
|
106
|
+
group: 'manage',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'uninstall',
|
|
110
|
+
usesCheckout: false,
|
|
111
|
+
usage: 'flowviant uninstall [--purge] [--yes]',
|
|
112
|
+
summary: 'Remove every flowviant copy on this computer. Logins are kept unless you add --purge.',
|
|
113
|
+
details: ['--purge also disconnect this computer from its projects and delete ~/.flowviant', '--yes do not ask first'],
|
|
114
|
+
group: 'manage',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: 'gh-auth',
|
|
118
|
+
usesCheckout: false,
|
|
119
|
+
usage: 'flowviant gh-auth',
|
|
120
|
+
summary: 'Sign in the GitHub CLI flowviant uses to open pull requests.',
|
|
121
|
+
group: 'manage',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'version',
|
|
125
|
+
usesCheckout: false,
|
|
126
|
+
usage: 'flowviant --version',
|
|
127
|
+
summary: 'Print the installed version.',
|
|
128
|
+
group: 'manage',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'help',
|
|
132
|
+
usesCheckout: false,
|
|
133
|
+
usage: 'flowviant help [command]',
|
|
134
|
+
summary: 'This list, or one command in detail.',
|
|
135
|
+
group: 'manage',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'shot',
|
|
139
|
+
usesCheckout: false,
|
|
140
|
+
usage: 'flowviant shot <url> [--out <file>]',
|
|
141
|
+
summary: 'Screenshot a running page with a headless browser (agents use it to see their change).',
|
|
142
|
+
hidden: true,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'mcp',
|
|
146
|
+
usesCheckout: false,
|
|
147
|
+
usage: 'flowviant mcp <subcommand>',
|
|
148
|
+
summary: "Manage the project's MCP connectors from a script.",
|
|
149
|
+
hidden: true,
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
/** Rows cli.mjs answers itself rather than through HANDLERS: `version` and
|
|
154
|
+
* `help` before any command runs, `start` as the fall-through daemon path. */
|
|
155
|
+
export const BUILTIN_COMMANDS = ['start', 'help', 'version'];
|
|
156
|
+
|
|
157
|
+
export const ALIASES = { '--version': 'version', '-v': 'version', '--help': 'help', '-h': 'help' };
|
|
158
|
+
|
|
159
|
+
export function findCommand(name) {
|
|
160
|
+
const key = ALIASES[name] ?? name;
|
|
161
|
+
return COMMANDS.find((cmd) => cmd.name === key) ?? null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Does `--dir` move the process into the checkout before this word runs?
|
|
166
|
+
* Anything the table does not name — a flag such as `--project`, or no word at
|
|
167
|
+
* all — is the start path, which serves the checkout. (An unknown word is
|
|
168
|
+
* refused after the move, exactly as before.)
|
|
169
|
+
*/
|
|
170
|
+
export function commandUsesCheckout(word) {
|
|
171
|
+
const cmd = findCommand(word);
|
|
172
|
+
return cmd ? cmd.usesCheckout : true;
|
|
173
|
+
}
|
package/bin/lib/config.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { randomBytes } from 'node:crypto';
|
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import {
|
|
7
|
+
import { hostname } from 'node:os';
|
|
8
|
+
import { measureMachineLimits } from './machineLimits.mjs';
|
|
8
9
|
|
|
9
10
|
// Read the daemon's version from its OWN package.json (always shipped in the npm
|
|
10
11
|
// tarball) — never hardcode it. The hardcoded constant drifted: it sat at
|
|
@@ -36,7 +37,7 @@ export const MODEL = process.env.FLOWVIANT_MODEL || 'opus';
|
|
|
36
37
|
// no-env-var path. An explicit --fleet flag or FLOWVIANT_FLEET env still wins.
|
|
37
38
|
// Since 0.55.0 the store holds MANY projects and resolution is BY REPO — see
|
|
38
39
|
// credentials.mjs for the whole rule. `CREDENTIAL` carries the resolution so
|
|
39
|
-
//
|
|
40
|
+
// startProject.mjs can turn an ambiguity into a picker instead of a guess.
|
|
40
41
|
import { resolveStoredCredential } from './credentials.mjs';
|
|
41
42
|
export const CREDENTIAL = resolveStoredCredential();
|
|
42
43
|
|
|
@@ -77,42 +78,10 @@ export const STREAM_URL =
|
|
|
77
78
|
FLEET_URL.replace(/\/agents(\/?)$/, '/stream$1').replace(/^http/, 'ws');
|
|
78
79
|
export const POLL_SECONDS = Number(process.env.POLL_SECONDS || 20);
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
* 4GB container on a 256GB box reads 256GB and cheerfully oversubscribes until
|
|
85
|
-
* the OOM killer picks a victim — which, because it picks by resident size, is
|
|
86
|
-
* frequently not the task that caused it. cgroup v2 publishes the real limits,
|
|
87
|
-
* so read those first and treat the os module as the fallback it is.
|
|
88
|
-
*/
|
|
89
|
-
function machineLimits() {
|
|
90
|
-
const readCgroup = (f) => {
|
|
91
|
-
try {
|
|
92
|
-
return readFileSync(`/sys/fs/cgroup/${f}`, 'utf8').trim();
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
let memBytes = totalmem();
|
|
98
|
-
const memMax = readCgroup('memory.max');
|
|
99
|
-
if (memMax && memMax !== 'max') {
|
|
100
|
-
const n = Number(memMax);
|
|
101
|
-
if (Number.isFinite(n) && n > 0) memBytes = Math.min(memBytes, n);
|
|
102
|
-
}
|
|
103
|
-
let cores = cpus().length || 2;
|
|
104
|
-
// "<quota> <period>" in microseconds, or "max <period>" for unlimited.
|
|
105
|
-
const cpuMax = readCgroup('cpu.max');
|
|
106
|
-
if (cpuMax && !cpuMax.startsWith('max')) {
|
|
107
|
-
const [q, p] = cpuMax.split(/\s+/).map(Number);
|
|
108
|
-
if (Number.isFinite(q) && Number.isFinite(p) && p > 0) {
|
|
109
|
-
cores = Math.max(1, Math.min(cores, Math.floor(q / p)));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return { memBytes, cores };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export const MACHINE = machineLimits();
|
|
81
|
+
// What this machine can actually be given — the cgroup's limits before the
|
|
82
|
+
// host's. The rule and its reader live in machineLimits.mjs; this is the
|
|
83
|
+
// measured result every importer of `MACHINE` has always read.
|
|
84
|
+
export const MACHINE = measureMachineLimits();
|
|
116
85
|
|
|
117
86
|
/**
|
|
118
87
|
* How many tasks THIS MACHINE will build at once.
|
|
@@ -167,7 +136,7 @@ export const MACHINE = machineLimits();
|
|
|
167
136
|
* behind it. Most do not — an agent turn is a CLI that reads, edits and
|
|
168
137
|
* commits, and it is API-bound for nearly all of its life. 1GB per turn with
|
|
169
138
|
* the same 2GB held back for the operating system is the honest sizing, and
|
|
170
|
-
* the PRESSURE guard (
|
|
139
|
+
* the PRESSURE guard (pressurePolicy.mjs) is what catches the turn that really
|
|
171
140
|
* does hold gigabytes: it measures at the moment of spawning, which is a
|
|
172
141
|
* thing a static divisor cannot do.
|
|
173
142
|
* · `cores − 1` is the laptop assumption this comment already claims to have
|
|
@@ -290,7 +259,7 @@ export const PROCESS_STARTED_AT = new Date(
|
|
|
290
259
|
// --tokens) stood beside it and carried WORKER tokens into the pre-daemon loop;
|
|
291
260
|
// that principal owns zero tools since dispatch was deleted, and the kind can no
|
|
292
261
|
// longer be minted, so the plumbing went with the entrypoint (2026-08-19).
|
|
293
|
-
// `let`, because ES named imports are LIVE bindings: when
|
|
262
|
+
// `let`, because ES named imports are LIVE bindings: when startProject.mjs answers an
|
|
294
263
|
// ambiguous store with a picker, adoptStoredCredential updates every importer
|
|
295
264
|
// before the daemon touches the network. Everything before that point (the
|
|
296
265
|
// resolution, the flags) is settled synchronously at import, as it always was.
|
|
@@ -325,7 +294,7 @@ export let FLEET_TOKEN =
|
|
|
325
294
|
* hashes would render as a confident "these boxes differ" beside a box holding
|
|
326
295
|
* the identical value. Ignorance renders nothing.
|
|
327
296
|
*
|
|
328
|
-
* `let`, for the same reason `FLEET_TOKEN` is: when
|
|
297
|
+
* `let`, for the same reason `FLEET_TOKEN` is: when startProject.mjs answers an
|
|
329
298
|
* ambiguous store with a picker, `adoptStoredCredential` is what settles both,
|
|
330
299
|
* and ES named imports are LIVE bindings so every importer sees the answer.
|
|
331
300
|
* A frozen copy read at import would salt a picked project's report with null
|
|
@@ -356,7 +325,7 @@ export function storedCredentialInUse() {
|
|
|
356
325
|
return Boolean(CREDENTIAL.entry?.fleetToken) && CREDENTIAL.entry.fleetToken === FLEET_TOKEN;
|
|
357
326
|
}
|
|
358
327
|
|
|
359
|
-
/**
|
|
328
|
+
/** startProject.mjs's picker chose. Must run BEFORE runFleetDaemon — nothing here
|
|
360
329
|
* re-authenticates a connection already made. */
|
|
361
330
|
export function adoptStoredCredential(entry) {
|
|
362
331
|
if (entry?.fleetToken) FLEET_TOKEN = entry.fleetToken;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHEN A CREDENTIAL WAS STORED, as a date a person can match against their own
|
|
3
|
+
* memory of setting it up ("I connected that one last month") — `Sep 16`.
|
|
4
|
+
*
|
|
5
|
+
* One home because three terminal sites spelled it independently (SOLID audit
|
|
6
|
+
* 2026-09-26, F168): the collision suffix in `credentials.mjs`
|
|
7
|
+
* (`projectRowLabel`), the `machines` listing, and `flowviant projects` in
|
|
8
|
+
* cli.mjs — each with its own copy of the en-US short-month format and its own
|
|
9
|
+
* decision about when the clause is dropped. The decision is the part that
|
|
10
|
+
* matters and is stated once here: ABSENT OR UNPARSEABLE STAYS ABSENT (null).
|
|
11
|
+
* A credential stored before `savedAt` existed has no date, and inventing one
|
|
12
|
+
* would be the listing asserting a day nobody recorded.
|
|
13
|
+
*
|
|
14
|
+
* Each caller places the returned date in its own sentence; only the date is
|
|
15
|
+
* shared. A leaf module on purpose: `credentials.mjs` imports it, so it must
|
|
16
|
+
* import nothing back (a cycle through the credential store).
|
|
17
|
+
*/
|
|
18
|
+
export function connectedOn(iso) {
|
|
19
|
+
const t = Date.parse(iso ?? '');
|
|
20
|
+
if (Number.isNaN(t)) return null;
|
|
21
|
+
return new Date(t).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
22
|
+
}
|
package/bin/lib/credentials.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* binds it (headless keeps the old behaviour —
|
|
34
34
|
* a systemd restart must not hang on a prompt).
|
|
35
35
|
* anything else a CHOICE, never a guess: the CLI lists every
|
|
36
|
-
* stored project and asks (
|
|
36
|
+
* stored project and asks (startProject.mjs), or — with
|
|
37
37
|
* no TTY — refuses in words that name them all.
|
|
38
38
|
*
|
|
39
39
|
* The repoRoot binding is a SAFETY line, not bookkeeping: serving project X
|
|
@@ -49,7 +49,9 @@ import { mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from
|
|
|
49
49
|
import { homedir } from 'node:os';
|
|
50
50
|
import { join } from 'node:path';
|
|
51
51
|
import { printable } from './printable.mjs';
|
|
52
|
+
import { connectedOn } from './credentialDate.mjs';
|
|
52
53
|
import { terminalCommand } from './launchCommand.mjs';
|
|
54
|
+
import { samePath } from './repoPath.mjs';
|
|
53
55
|
|
|
54
56
|
const CRED_DIR = join(homedir(), '.flowviant');
|
|
55
57
|
const CRED_FILE = join(CRED_DIR, 'credentials.json');
|
|
@@ -86,19 +88,6 @@ export function detectRepoRoot(cwd = process.cwd()) {
|
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
/** Same directory whatever it is spelled as — the instance.mjs rule. */
|
|
90
|
-
function samePath(a, b) {
|
|
91
|
-
if (!a || !b) return false;
|
|
92
|
-
const norm = (v) => {
|
|
93
|
-
try {
|
|
94
|
-
return realpathSync(v);
|
|
95
|
-
} catch {
|
|
96
|
-
return String(v).replace(/\/+$/, '');
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
return norm(a) === norm(b);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
91
|
/**
|
|
103
92
|
* Every stored project, NORMALIZED: the v2 map, plus the legacy trio surfaced
|
|
104
93
|
* as an entry when no map row carries its token (a pre-0.55.0 file is one
|
|
@@ -187,11 +176,8 @@ export function projectRowLabel(e, entries) {
|
|
|
187
176
|
Array.isArray(entries) &&
|
|
188
177
|
entries.filter((x) => x !== e && projectLabel(x) === label).length > 0;
|
|
189
178
|
if (!collides) return label;
|
|
190
|
-
const
|
|
191
|
-
const when =
|
|
192
|
-
d && !Number.isNaN(d.getTime())
|
|
193
|
-
? `, connected ${d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`
|
|
194
|
-
: '';
|
|
179
|
+
const connected = connectedOn(e?.savedAt);
|
|
180
|
+
const when = connected ? `, connected ${connected}` : '';
|
|
195
181
|
return `${label} (id ${String(e?.projectId ?? '').slice(0, 8)}…${when})`;
|
|
196
182
|
}
|
|
197
183
|
|
|
@@ -363,6 +349,19 @@ export function selectStoredProject(projectId, { bindRepoRoot } = {}) {
|
|
|
363
349
|
});
|
|
364
350
|
}
|
|
365
351
|
|
|
352
|
+
/**
|
|
353
|
+
* WHICH STORED PROJECTS AN ID NAMES — the full project id, or a prefix of at
|
|
354
|
+
* least six characters. The one spelling of the boundary for both the lookup
|
|
355
|
+
* (`matchStoredProject`, which falls back to a name when no id matches) and
|
|
356
|
+
* the destructive forget (`forgetStoredProject`, which never matches a name):
|
|
357
|
+
* two copies of a rule that decides which credential gets DELETED is a second
|
|
358
|
+
* place for the boundary to drift (SOLID audit 2026-09-26, F169). Returns every
|
|
359
|
+
* hit — each caller refuses more than one in its own words.
|
|
360
|
+
*/
|
|
361
|
+
function storedProjectsById(q, entries) {
|
|
362
|
+
return (entries ?? []).filter((e) => e.projectId === q || (q.length >= 6 && e.projectId.startsWith(q)));
|
|
363
|
+
}
|
|
364
|
+
|
|
366
365
|
/**
|
|
367
366
|
* FORGET A STORED CREDENTIAL — `flowviant machines --forget <id>` (2026-09-19).
|
|
368
367
|
*
|
|
@@ -386,9 +385,7 @@ export function selectStoredProject(projectId, { bindRepoRoot } = {}) {
|
|
|
386
385
|
export function forgetStoredProject(ref) {
|
|
387
386
|
const q = String(ref ?? '').trim();
|
|
388
387
|
if (!q) return { error: 'empty --forget value' };
|
|
389
|
-
const hits = listStoredProjects()
|
|
390
|
-
(e) => e.projectId === q || (q.length >= 6 && e.projectId.startsWith(q))
|
|
391
|
-
);
|
|
388
|
+
const hits = storedProjectsById(q, listStoredProjects());
|
|
392
389
|
if (hits.length === 0) return { error: `no stored project matches "${q}"` };
|
|
393
390
|
if (hits.length > 1) {
|
|
394
391
|
return { error: `"${q}" matches ${hits.length} stored projects — use the full project id` };
|
|
@@ -431,9 +428,7 @@ export function matchStoredProject(ref) {
|
|
|
431
428
|
const entries = listStoredProjects();
|
|
432
429
|
const q = String(ref ?? '').trim();
|
|
433
430
|
if (!q) return { error: 'empty --project value' };
|
|
434
|
-
const byId = entries
|
|
435
|
-
(e) => e.projectId === q || (q.length >= 6 && e.projectId.startsWith(q))
|
|
436
|
-
);
|
|
431
|
+
const byId = storedProjectsById(q, entries);
|
|
437
432
|
const byName = entries.filter((e) => e.name && e.name.toLowerCase() === q.toLowerCase());
|
|
438
433
|
const hits = byId.length > 0 ? byId : byName;
|
|
439
434
|
if (hits.length === 1) return { entry: hits[0] };
|
|
@@ -447,7 +442,7 @@ export function matchStoredProject(ref) {
|
|
|
447
442
|
* this returns. Shapes:
|
|
448
443
|
* { entry, source: 'project-flag' | 'repo' | 'only', needsConfirm? }
|
|
449
444
|
* { choices, reason: 'no-match' | 'multiple-bound' | 'outside-repo', repoRoot }
|
|
450
|
-
* (`multiple-bound` is a REFUSAL
|
|
445
|
+
* (`multiple-bound` is a REFUSAL on the start path (startProject.mjs) since 2026-09-23 — a directory
|
|
451
446
|
* serves one project — and never a picker; the shape is kept because it
|
|
452
447
|
* names the projects the refusal has to print)
|
|
453
448
|
* { none: true }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE DAEMON'S OUTPUT: bounded per-project logs and the desktop JSONL events
|
|
3
|
+
* (2026-09-26, SOLID F061).
|
|
4
|
+
*
|
|
5
|
+
* Split out of desktopContract.mjs, which also built the status readout — so a
|
|
6
|
+
* change to how the daemon writes its output (console redirection, log
|
|
7
|
+
* rotation, the JSON event stream) and a change to the status wire edited one
|
|
8
|
+
* module. This one owns output only; it is handed the log path and never
|
|
9
|
+
* decides where per-project files live (daemonState.mjs does).
|
|
10
|
+
*/
|
|
11
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from 'node:fs';
|
|
12
|
+
import { dirname } from 'node:path';
|
|
13
|
+
import { format } from 'node:util';
|
|
14
|
+
|
|
15
|
+
export const MAX_DAEMON_LOG_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
|
|
17
|
+
let events = false;
|
|
18
|
+
let eventWriter = process.stdout.write.bind(process.stdout);
|
|
19
|
+
export function enableMachineEvents(write = eventWriter) { events = true; eventWriter = write; }
|
|
20
|
+
export function emitMachineEvent(value) {
|
|
21
|
+
if (events) eventWriter(`${JSON.stringify(value)}\n`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** All human output stays on its existing stream in terminal mode. In desktop
|
|
25
|
+
* mode stdout is reserved for JSONL and those same lines go to stderr. Every
|
|
26
|
+
* line is also appended to `logPath`, rotated at MAX_DAEMON_LOG_BYTES. */
|
|
27
|
+
export function installDaemonLogging(logPath, { jsonEvents = false } = {}) {
|
|
28
|
+
if (!logPath) return;
|
|
29
|
+
if (jsonEvents) {
|
|
30
|
+
enableMachineEvents();
|
|
31
|
+
process.stdout.write = (...args) => process.stderr.write(...args);
|
|
32
|
+
}
|
|
33
|
+
const original = { log: console.log, info: console.info, warn: console.warn, error: console.error };
|
|
34
|
+
for (const method of Object.keys(original)) {
|
|
35
|
+
console[method] = (...args) => {
|
|
36
|
+
const line = format(...args);
|
|
37
|
+
try {
|
|
38
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
39
|
+
const encoded = Buffer.from(`${line}\n`);
|
|
40
|
+
const tail = Buffer.from('\n[truncated]\n');
|
|
41
|
+
const chunk = encoded.length > MAX_DAEMON_LOG_BYTES
|
|
42
|
+
? Buffer.concat([encoded.subarray(0, MAX_DAEMON_LOG_BYTES - tail.length), tail])
|
|
43
|
+
: encoded;
|
|
44
|
+
let size = 0;
|
|
45
|
+
try { size = statSync(logPath).size; } catch { /* new file */ }
|
|
46
|
+
if (size + chunk.length > MAX_DAEMON_LOG_BYTES) {
|
|
47
|
+
try { renameSync(logPath, `${logPath}.1`); } catch { /* no old log */ }
|
|
48
|
+
}
|
|
49
|
+
appendFileSync(logPath, chunk, { mode: 0o600 });
|
|
50
|
+
} catch { /* logging must never stop service */ }
|
|
51
|
+
if (jsonEvents) process.stderr.write(`${line}\n`);
|
|
52
|
+
else original[method](...args);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHERE A PROJECT'S DAEMON KEEPS ITS LOCAL READOUT, AND WHAT IT WRITES THERE
|
|
3
|
+
* (2026-09-26, SOLID F061).
|
|
4
|
+
*
|
|
5
|
+
* The per-project state file (holder, last poll, CLI limits) and log file
|
|
6
|
+
* under ~/.flowviant, named by a hash of the project id. Split out of
|
|
7
|
+
* desktopContract.mjs so the status readout only READS this, and so the
|
|
8
|
+
* daemon's writers (fleet.mjs, workAgentTurnExecution.mjs) do not import the status
|
|
9
|
+
* builder. A limit change is announced as a machine event (daemonLogging.mjs).
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { emitMachineEvent } from './daemonLogging.mjs';
|
|
16
|
+
import { instanceLockPath } from './instance.mjs';
|
|
17
|
+
|
|
18
|
+
const store = () => join(homedir(), '.flowviant');
|
|
19
|
+
const key = (projectId) => createHash('sha256').update(String(projectId)).digest('hex').slice(0, 16);
|
|
20
|
+
export const daemonLogPath = (projectId) => join(store(), `daemon-${key(projectId)}.log`);
|
|
21
|
+
export const daemonStatePath = (projectId) => join(store(), `daemon-${key(projectId)}.state.json`);
|
|
22
|
+
|
|
23
|
+
export function readDaemonState(projectId) {
|
|
24
|
+
try {
|
|
25
|
+
const value = JSON.parse(readFileSync(daemonStatePath(projectId), 'utf8'));
|
|
26
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
27
|
+
} catch { return {}; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function writeDaemonState(projectId, change) {
|
|
31
|
+
if (!projectId) return;
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(store(), { recursive: true });
|
|
34
|
+
const path = daemonStatePath(projectId);
|
|
35
|
+
const current = readDaemonState(projectId);
|
|
36
|
+
const next = { ...current, ...change, pid: process.pid };
|
|
37
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
38
|
+
writeFileSync(tmp, JSON.stringify(next), { mode: 0o600 });
|
|
39
|
+
renameSync(tmp, path);
|
|
40
|
+
} catch { /* a local readout must never interrupt a daemon */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function setRuntimeLimit(projectId, runtime, words) {
|
|
44
|
+
if (!projectId || !runtime) return false;
|
|
45
|
+
const state = readDaemonState(projectId);
|
|
46
|
+
const limits = { ...(state.limits ?? {}) };
|
|
47
|
+
if (words == null) {
|
|
48
|
+
if (!Object.hasOwn(limits, runtime)) return false;
|
|
49
|
+
delete limits[runtime];
|
|
50
|
+
} else {
|
|
51
|
+
if (limits[runtime] === words) return false;
|
|
52
|
+
limits[runtime] = words;
|
|
53
|
+
}
|
|
54
|
+
writeDaemonState(projectId, { limits });
|
|
55
|
+
emitMachineEvent({ event: words == null ? 'limit-cleared' : 'limit-hit', runtime, ...(words == null ? {} : { message: words }) });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The pid in the instance lock for this credential, or null. */
|
|
60
|
+
export function daemonLockPid(token) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(readFileSync(instanceLockPath(token), 'utf8')).pid;
|
|
63
|
+
} catch { return null; }
|
|
64
|
+
}
|