flowviant 0.79.0 → 0.81.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 +2 -2
- package/bin/lib/claudeAuth.mjs +164 -0
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/fleet.mjs +25 -2
- package/bin/lib/vault.mjs +1 -1
- package/package.json +2 -2
package/bin/cli.mjs
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* running?", which otherwise ends in a pid hunt through `ps`.
|
|
27
27
|
*
|
|
28
28
|
* The daemon: install ONCE with a machine credential, then work entirely from
|
|
29
|
-
* Flowviant. It polls GET /api/
|
|
29
|
+
* Flowviant. It polls GET /api/fleet/agents, and the roster hands it the
|
|
30
30
|
* project's SESSIONS — the Workbench's tabs. Each session gets one persistent
|
|
31
31
|
* git worktree on its own `session/<id>` branch, held across turns (never reset
|
|
32
32
|
* to base: the branch outlives the tab). A turn spawns the session's CLI with a
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
*
|
|
45
45
|
* Env:
|
|
46
46
|
* FLOWVIANT_FLEET the machine credential (or use `flowviant login`).
|
|
47
|
-
* FLOWVIANT_API_URL default https://api.flowviant.com/api
|
|
47
|
+
* FLOWVIANT_API_URL default https://api.flowviant.com/api
|
|
48
48
|
* FLOWVIANT_MCP_URL default <API_URL>/mcp
|
|
49
49
|
* FLOWVIANT_FLEET_URL default <API_URL>/fleet/agents
|
|
50
50
|
* RECONCILE_SECONDS roster poll cadence (default 10)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH CREDENTIAL THIS MACHINE'S CLI WILL RESOLVE — presence and expiry only,
|
|
3
|
+
* never a token value, and never a judgement about which one is correct.
|
|
4
|
+
*
|
|
5
|
+
* ── THE CONFUSION THIS EXISTS TO END ──
|
|
6
|
+
*
|
|
7
|
+
* A turn failed with the CLI's own words relayed verbatim:
|
|
8
|
+
*
|
|
9
|
+
* Failed to authenticate: OAuth session expired and could not be refreshed
|
|
10
|
+
*
|
|
11
|
+
* and the operator did the obvious thing — opened a terminal on that same
|
|
12
|
+
* machine, typed `claude`, and watched it work. Both observations were true at
|
|
13
|
+
* once, and nothing in the product could explain how, because the one fact that
|
|
14
|
+
* reconciles them is invisible from a browser: THE DAEMON'S CLI AND YOUR SHELL'S
|
|
15
|
+
* CLI DO NOT ALWAYS RESOLVE THE SAME CREDENTIAL.
|
|
16
|
+
*
|
|
17
|
+
* Two ways they diverge, and this module reports both:
|
|
18
|
+
*
|
|
19
|
+
* · THE ENVIRONMENT. `claude.mjs` spawns the CLI with `{...process.env}` on
|
|
20
|
+
* purpose — its header says so: "the CLI's own credentials live in this
|
|
21
|
+
* environment, and handing it a curated one signs it out". It also stopped
|
|
22
|
+
* deleting ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN deliberately, because on
|
|
23
|
+
* a machine the project leaves running an inherited org key is the POINT.
|
|
24
|
+
* The consequence is that an auth variable in the daemon's environment is
|
|
25
|
+
* handed to every turn — and `process.env` is a SNAPSHOT taken when the
|
|
26
|
+
* daemon started, so a variable exported weeks ago is still in there long
|
|
27
|
+
* after it left your shell. A stale one fails in a way a fresh `claude`
|
|
28
|
+
* never reproduces.
|
|
29
|
+
* · THE HOME. The CLI reads its store from $HOME. A daemon under systemd, a
|
|
30
|
+
* different user, or a container has a different one than the shell you
|
|
31
|
+
* tested in, so "I am logged in" was measured on the wrong file.
|
|
32
|
+
*
|
|
33
|
+
* ── WHAT THIS IS NOT ──
|
|
34
|
+
*
|
|
35
|
+
* NOT A POLICY. Flowviant does not pick a credential, does not prefer the
|
|
36
|
+
* subscription over a key, and does not call either one wrong — `claude.mjs`
|
|
37
|
+
* settled that: "Which credential is correct, and whether an account may be
|
|
38
|
+
* shared, is between the operator and the vendor." This reports what is
|
|
39
|
+
* PRESENT so a person can see the divergence; it never resolves it.
|
|
40
|
+
*
|
|
41
|
+
* NOT A PRECEDENCE CLAIM. Which source the CLI actually prefers is the CLI's
|
|
42
|
+
* own business and it is free to change it. We report that both exist, which is
|
|
43
|
+
* the fact that explains the symptom, and we do not assert which one won.
|
|
44
|
+
*
|
|
45
|
+
* NOT A TOKEN READER. Values never leave — not to the server, not to a log, not
|
|
46
|
+
* into an error. Presence, the variable's NAME, and the refresh expiry, which
|
|
47
|
+
* is a date.
|
|
48
|
+
*
|
|
49
|
+
* ── THE THREE-STATE RULE, WHICH THIS FILE LIVES OR DIES BY ──
|
|
50
|
+
*
|
|
51
|
+
* `source: 'unknown'` is NOT "signed out". Claude Code can keep credentials in
|
|
52
|
+
* an OS keychain (macOS especially), where a file simply does not exist and
|
|
53
|
+
* everything is fine. Reporting a confident "no login" for a machine that is
|
|
54
|
+
* working perfectly is precisely the invented state this product forbids, and
|
|
55
|
+
* it would be worse than saying nothing. Absent means absent; the surface
|
|
56
|
+
* renders nothing for it.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { readFileSync } from 'node:fs';
|
|
60
|
+
import { join } from 'node:path';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The variables that can carry auth into the CLI from the environment.
|
|
64
|
+
*
|
|
65
|
+
* The first two are the pair `claude.mjs` documents itself as deliberately NOT
|
|
66
|
+
* deleting, and both appear in childEnv.mjs's allowlist. The third is Claude
|
|
67
|
+
* Code's own headless/CI token. Names only ever leave this module — a NAME is
|
|
68
|
+
* what a person greps for, and it is not a secret.
|
|
69
|
+
*/
|
|
70
|
+
export const AUTH_ENV_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN'];
|
|
71
|
+
|
|
72
|
+
/** Claude Code's OAuth store, relative to the HOME the CLI will be handed. */
|
|
73
|
+
const CRED_REL = ['.claude', '.credentials.json'];
|
|
74
|
+
|
|
75
|
+
/** A stamp that may be seconds or milliseconds, to ISO — or null if it is
|
|
76
|
+
* neither. Same tolerance the rest of the daemon applies to numbers it did not
|
|
77
|
+
* write: an unreadable stamp is ABSENT, never zero and never "now". */
|
|
78
|
+
function isoFromStamp(v) {
|
|
79
|
+
const n = Number(v);
|
|
80
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
81
|
+
const ms = n > 1e11 ? n : n * 1000;
|
|
82
|
+
const d = new Date(ms);
|
|
83
|
+
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* What the CLI on this machine will resolve, as far as it can be seen without
|
|
88
|
+
* spending anything: no process spawned, no request made, no quota touched.
|
|
89
|
+
* Just this process's own environment and one file's metadata.
|
|
90
|
+
*
|
|
91
|
+
* `env` and `home` are parameters rather than reads of `process` so the tests
|
|
92
|
+
* can drive every branch — and so the caller passes the SAME environment the
|
|
93
|
+
* child will actually receive, which is the whole point of the report.
|
|
94
|
+
*/
|
|
95
|
+
export function claudeAuthContext({ env = process.env, home = null } = {}) {
|
|
96
|
+
const HOME = home ?? env.HOME ?? env.USERPROFILE ?? null;
|
|
97
|
+
|
|
98
|
+
// Presence, by NAME. An empty or whitespace-only value is not set: an
|
|
99
|
+
// exported-but-blank variable is a common way to try to UNSET one, and
|
|
100
|
+
// reporting it as present would send somebody hunting a variable that is
|
|
101
|
+
// doing nothing.
|
|
102
|
+
const envVars = AUTH_ENV_VARS.filter((n) => typeof env[n] === 'string' && env[n].trim() !== '');
|
|
103
|
+
|
|
104
|
+
let hasFile = false;
|
|
105
|
+
let refreshExpiresAt = null;
|
|
106
|
+
let accessExpiresAt = null;
|
|
107
|
+
let subscriptionType = null;
|
|
108
|
+
if (HOME) {
|
|
109
|
+
try {
|
|
110
|
+
const raw = readFileSync(join(HOME, ...CRED_REL), 'utf8');
|
|
111
|
+
const oauth = JSON.parse(raw)?.claudeAiOauth;
|
|
112
|
+
if (oauth && typeof oauth === 'object') {
|
|
113
|
+
hasFile = true;
|
|
114
|
+
refreshExpiresAt = isoFromStamp(oauth.refreshTokenExpiresAt);
|
|
115
|
+
accessExpiresAt = isoFromStamp(oauth.expiresAt);
|
|
116
|
+
// A plan name is not a secret and it is the one field that tells an
|
|
117
|
+
// operator WHICH kind of account the machine is spending — the thing
|
|
118
|
+
// "the machine is shared and so is its CLI login" makes everyone's
|
|
119
|
+
// business. Anything unexpected is dropped rather than relayed.
|
|
120
|
+
if (typeof oauth.subscriptionType === 'string' && oauth.subscriptionType.length <= 32) {
|
|
121
|
+
subscriptionType = oauth.subscriptionType;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Absent, unreadable, or not JSON. All three mean the same thing here:
|
|
126
|
+
// we cannot see a file, which is NOT the same as there not being a
|
|
127
|
+
// login. See the three-state rule in this file's header.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* THE SOURCE, and the honest thing to say about it.
|
|
133
|
+
*
|
|
134
|
+
* 'env' — at least one auth variable is set. Reported first because it
|
|
135
|
+
* is the state that is invisible from a shell and therefore the
|
|
136
|
+
* one that misleads; NOT because we know it wins.
|
|
137
|
+
* 'file' — no variable, and an OAuth store we could read.
|
|
138
|
+
* 'unknown' — neither. A keychain machine lives here, and so does a machine
|
|
139
|
+
* with no login at all. We cannot tell them apart, so we say
|
|
140
|
+
* nothing about which it is.
|
|
141
|
+
*/
|
|
142
|
+
const source = envVars.length ? 'env' : hasFile ? 'file' : 'unknown';
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
/** The OS user the daemon runs as — the other half of "which store". */
|
|
146
|
+
user: env.USER ?? env.LOGNAME ?? env.USERNAME ?? null,
|
|
147
|
+
home: HOME,
|
|
148
|
+
source,
|
|
149
|
+
/** NAMES ONLY. Never a value, never a prefix, never a length. */
|
|
150
|
+
envVars,
|
|
151
|
+
/** Is there a login sitting behind the variable that may be shadowing it?
|
|
152
|
+
* This — not `source` on its own — is the confusing state, and it is the
|
|
153
|
+
* only one the surface is allowed to raise unprompted. */
|
|
154
|
+
envOverridesLogin: envVars.length > 0 && hasFile,
|
|
155
|
+
/** The 22-day clock. Null when we cannot see the file, which includes every
|
|
156
|
+
* keychain machine, so null must never render as "expired". */
|
|
157
|
+
refreshExpiresAt,
|
|
158
|
+
/** The ~12-hour clock. Reported for completeness and deliberately NOT what
|
|
159
|
+
* any warning is built from: it lapses constantly and refreshes itself, so
|
|
160
|
+
* a note built on it would cry wolf twice a day. */
|
|
161
|
+
accessExpiresAt,
|
|
162
|
+
subscriptionType,
|
|
163
|
+
};
|
|
164
|
+
}
|
package/bin/lib/config.mjs
CHANGED
|
@@ -41,7 +41,28 @@ function argFlag(name) {
|
|
|
41
41
|
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
/**
|
|
45
|
+
* THE SERVER'S API ROOT.
|
|
46
|
+
*
|
|
47
|
+
* `/api`, not `/api/v2`. The server collapsed its two prefixes into one
|
|
48
|
+
* namespace on 2026-09-08 — they had held DISJOINT resources and neither was
|
|
49
|
+
* ever a version of the other, so there was nothing to choose between.
|
|
50
|
+
*
|
|
51
|
+
* ── DEPLOY ORDER IS NOW LOAD-BEARING FOR THIS LINE ──
|
|
52
|
+
*
|
|
53
|
+
* A daemon on this version calls `/api/fleet/agents`. An older SERVER does not
|
|
54
|
+
* serve that path, so publishing this release before the server that answers it
|
|
55
|
+
* 404s the roster poll for anyone who updates. The order in the app repo's
|
|
56
|
+
* ARCHITECTURE.md already says it and now it matters: deploy `flowviant-api`
|
|
57
|
+
* FIRST, publish this SECOND. The same applies to a rollback — rolling the
|
|
58
|
+
* server back past that merge strands every daemon at this version or newer.
|
|
59
|
+
*
|
|
60
|
+
* The other direction is safe and needs nothing: the server keeps `/api/v2` as
|
|
61
|
+
* an alias onto the same router precisely because every daemon published before
|
|
62
|
+
* this one has that string baked in and cannot be upgraded by a deploy. That
|
|
63
|
+
* alias retires when the app's `DAEMON_MIN_VERSION` clears this release.
|
|
64
|
+
*/
|
|
65
|
+
const API_BASE = process.env.FLOWVIANT_API_URL || 'https://api.flowviant.com/api';
|
|
45
66
|
export const MCP_URL = process.env.FLOWVIANT_MCP_URL || `${API_BASE}/mcp`;
|
|
46
67
|
export const FLEET_URL = process.env.FLOWVIANT_FLEET_URL || `${API_BASE}/fleet/agents`;
|
|
47
68
|
// Push channel: the daemon holds this WebSocket open and the server nudges it
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fleet daemon. Install ONCE with a fleet credential; manage everything from
|
|
3
|
-
* Flowviant. The daemon polls GET /api/
|
|
3
|
+
* Flowviant. The daemon polls GET /api/fleet/agents, reconciles one persistent
|
|
4
4
|
* git worktree + worker loop per roster agent, rotates each worker's short-lived
|
|
5
5
|
* MCP token, and only spawns Claude when the server says an agent has work.
|
|
6
6
|
*/
|
|
@@ -85,6 +85,7 @@ import {
|
|
|
85
85
|
import { createWorkManager } from './work.mjs';
|
|
86
86
|
import { scanLocalSessions, ourConversationIds } from './localSessions.mjs';
|
|
87
87
|
import { repoState } from './repoState.mjs';
|
|
88
|
+
import { claudeAuthContext } from './claudeAuth.mjs';
|
|
88
89
|
|
|
89
90
|
async function fetchRoster(
|
|
90
91
|
haveIds,
|
|
@@ -327,7 +328,29 @@ async function maybeReportRepoState({ repoRoot, baseRef }) {
|
|
|
327
328
|
// time, so the value here is always current.
|
|
328
329
|
const state = repoState(repoRoot, baseRef);
|
|
329
330
|
if (!state) return; // not readable — say nothing rather than say "none"
|
|
330
|
-
|
|
331
|
+
/**
|
|
332
|
+
* WHICH CREDENTIAL THIS MACHINE'S CLI RESOLVES, on the same beat.
|
|
333
|
+
*
|
|
334
|
+
* It rides this report rather than getting an endpoint of its own because
|
|
335
|
+
* it is the same KIND of fact — something only the machine can see, pushed
|
|
336
|
+
* because a pull client can never be asked — and it inherits this
|
|
337
|
+
* function's three economies for free: scanned once a minute, deduped
|
|
338
|
+
* against the last ACCEPTED payload, and silent forever once an older
|
|
339
|
+
* server 404s.
|
|
340
|
+
*
|
|
341
|
+
* The dedup keeps it cheap: `claudeAuthContext` is presence plus two dates,
|
|
342
|
+
* so the string is stable across scans and only moves when something about
|
|
343
|
+
* the credential actually moves. It DOES move when the CLI refreshes its
|
|
344
|
+
* access token — that stamp is carried for diagnostics — which costs a
|
|
345
|
+
* write roughly twice a day and is the whole of the extra traffic. The
|
|
346
|
+
* WARNING is built on the 22-day refresh clock instead, so a note never
|
|
347
|
+
* fires on the ~12h cycle.
|
|
348
|
+
*
|
|
349
|
+
* NO VERSION FLOOR, and none is possible to need: this is a daemon→server
|
|
350
|
+
* report, and an older SERVER strips the unknown key in its zod parse and
|
|
351
|
+
* stores the rest exactly as before.
|
|
352
|
+
*/
|
|
353
|
+
payload = JSON.stringify({ ...state, auth: claudeAuthContext() });
|
|
331
354
|
} catch {
|
|
332
355
|
return; // a readout must never throw into the poll loop
|
|
333
356
|
}
|
package/bin/lib/vault.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* private `git init` so every pass is versioned locally for free — the user's
|
|
7
7
|
* repository is never touched.
|
|
8
8
|
*
|
|
9
|
-
* Sync protocol (POST /api/
|
|
9
|
+
* Sync protocol (POST /api/fleet/wiki-vault, fleet-token auth): only files
|
|
10
10
|
* whose sha256 changed since the last successful sync are uploaded, chunked;
|
|
11
11
|
* the LAST request carries the finalize.manifest of a completed full sweep so
|
|
12
12
|
* the server prunes pages the sweep no longer has. The last-synced hashes live
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Run your own coding CLIs as build agents for Flowviant
|
|
3
|
+
"version": "0.81.0",
|
|
4
|
+
"description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"flowviant": "bin/cli.mjs"
|