flowviant 0.80.0 → 0.82.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/lib/claudeAuth.mjs +164 -0
- package/bin/lib/fleet.mjs +24 -1
- package/bin/lib/prompts.mjs +50 -0
- package/bin/lib/work.mjs +20 -2
- package/package.json +2 -2
|
@@ -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/fleet.mjs
CHANGED
|
@@ -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/prompts.mjs
CHANGED
|
@@ -458,6 +458,43 @@ const kickoff = ({ message, askedByName, head, tail }) => {
|
|
|
458
458
|
return `${scaffold}${fence('WHAT THEY SAID', message)}\n\n${tail}`;
|
|
459
459
|
};
|
|
460
460
|
|
|
461
|
+
/**
|
|
462
|
+
* A CAPTURE TAB (server: work_session.kind='capture', flagged on the job as
|
|
463
|
+
* `capture: true`) — the board's "New task" conversation. Its whole job is
|
|
464
|
+
* turning what the person says into STAGED cards a human lands; it edits
|
|
465
|
+
* nothing, and the permission profile enforces that (PLAN_PERM: read-only +
|
|
466
|
+
* MCP — the same fence the scratch planner runs behind). This prompt is the
|
|
467
|
+
* QUALITY half; the token scope and the permission list are the safety.
|
|
468
|
+
*/
|
|
469
|
+
export const SYSTEM_CAPTURE = `You are the human's own Claude, in their repository, with ONE job: turn what
|
|
470
|
+
they say into well-cut task cards on their Flowviant board. You are READ-ONLY
|
|
471
|
+
here — read code freely to ground what you stage; the permission profile
|
|
472
|
+
refuses edits, and staging is the only write you have.
|
|
473
|
+
|
|
474
|
+
MECHANICS OF THIS CHAT:
|
|
475
|
+
|
|
476
|
+
1. NARRATE WHILE YOU WORK. Call stream_session_turn with short progress lines
|
|
477
|
+
as you read and stage. Your FINAL reply is delivered automatically when the
|
|
478
|
+
turn ends — do not repeat it through the tool. Keep replies short: what you
|
|
479
|
+
staged, or what you need to know — one line each.
|
|
480
|
+
2. STAGE, NEVER FILE. stage_card proposes a new card; stage_card_edit proposes
|
|
481
|
+
a change to an existing one (read_card first — never replace fields you
|
|
482
|
+
have not seen). Everything you stage waits in an area the person reviews
|
|
483
|
+
and lands themselves; nothing you do reaches the board directly.
|
|
484
|
+
3. ONE CARD PER SHIPPABLE UNIT. Break a big ask into the units that will build
|
|
485
|
+
it, in landing order. Never card-ify chatter, questions, or one unit split
|
|
486
|
+
thin.
|
|
487
|
+
4. DEDUPE FIRST, EVERY TIME. Before staging, call list_cards (the OPEN QUEUE —
|
|
488
|
+
capped, and cards agents already hold are absent from it) and list_staged
|
|
489
|
+
(what earlier chats left). If the work exists, say so and point at it;
|
|
490
|
+
stage_card_edit it if the ask adds something. Never stage a twin.
|
|
491
|
+
5. CLARIFY BEFORE STAGING. A vague ask gets one or two sharp questions and a
|
|
492
|
+
wait — never a staged guess. A clear ask gets staged without ceremony.
|
|
493
|
+
6. A GOOD CARD: a title naming the outcome, a brief a stranger could start
|
|
494
|
+
from, acceptance criteria only when the person stated (or the code shows)
|
|
495
|
+
what done means. No sizes, no owners, no statuses — none of those are
|
|
496
|
+
yours to set, here or anywhere.`;
|
|
497
|
+
|
|
461
498
|
export const WORK_TURN_KICKOFF = ({ sessionId, sessionName, message, askedByName }) =>
|
|
462
499
|
kickoff({
|
|
463
500
|
message,
|
|
@@ -468,6 +505,19 @@ export const WORK_TURN_KICKOFF = ({ sessionId, sessionName, message, askedByName
|
|
|
468
505
|
tail: `Stream your reply with stream_session_turn as you work.`,
|
|
469
506
|
});
|
|
470
507
|
|
|
508
|
+
/** The capture chat's kickoff — the same shape as a work turn's, with the
|
|
509
|
+
* head restating the posture so the message is read as capture input even
|
|
510
|
+
* deep in a long conversation. */
|
|
511
|
+
export const CAPTURE_TURN_KICKOFF = ({ sessionId, sessionName, message, askedByName }) =>
|
|
512
|
+
kickoff({
|
|
513
|
+
message,
|
|
514
|
+
askedByName,
|
|
515
|
+
head:
|
|
516
|
+
`Continue the task-capture chat${sessionName ? ` "${sessionName}"` : ''} — stage cards, change nothing.\n\n` +
|
|
517
|
+
`SESSION ID (pass this to stream_session_turn / the staging tools): ${sessionId}`,
|
|
518
|
+
tail: `Stream your reply with stream_session_turn as you work.`,
|
|
519
|
+
});
|
|
520
|
+
|
|
471
521
|
/** The plain tab's kickoff: no session id (there is no tool to pass it to)
|
|
472
522
|
* and no streaming instruction — the final message is the reply. */
|
|
473
523
|
export const WORK_TURN_KICKOFF_PLAIN = ({ sessionName, message, askedByName }) =>
|
package/bin/lib/work.mjs
CHANGED
|
@@ -52,7 +52,9 @@ import { c, note, ok, warn } from './ui.mjs';
|
|
|
52
52
|
import { mcpFor, runTurn } from './claude.mjs';
|
|
53
53
|
import {
|
|
54
54
|
SYSTEM_WORK,
|
|
55
|
+
SYSTEM_CAPTURE,
|
|
55
56
|
WORK_TURN_KICKOFF,
|
|
57
|
+
CAPTURE_TURN_KICKOFF,
|
|
56
58
|
SYSTEM_WORK_PLAIN,
|
|
57
59
|
WORK_TURN_KICKOFF_PLAIN,
|
|
58
60
|
SYSTEM_PLAN,
|
|
@@ -2982,6 +2984,14 @@ export function createWorkManager({
|
|
|
2982
2984
|
const message = [job.body, filesNote, adoptNote, carryNote]
|
|
2983
2985
|
.filter(Boolean)
|
|
2984
2986
|
.join('\n\n');
|
|
2987
|
+
// A CAPTURE chat (the board's New task conversation): its own
|
|
2988
|
+
// system prompt, its own kickoff, and the scratch planner's
|
|
2989
|
+
// READ-ONLY permission profile — the prompt says stage-never-file
|
|
2990
|
+
// and the profile is what makes "read-only" true rather than
|
|
2991
|
+
// asserted. Server-flagged per job; a server too old to flag it
|
|
2992
|
+
// simply runs an ordinary tab, which the web's version floor
|
|
2993
|
+
// prevents ever being offered.
|
|
2994
|
+
const captureTab = job.capture === true;
|
|
2985
2995
|
const turnArgs = {
|
|
2986
2996
|
// A plain tab has no tools to name and no session id to pass —
|
|
2987
2997
|
// its kickoff asks for one complete report instead of a stream.
|
|
@@ -2991,18 +3001,26 @@ export function createWorkManager({
|
|
|
2991
3001
|
message,
|
|
2992
3002
|
askedByName: job.askedByName,
|
|
2993
3003
|
})
|
|
2994
|
-
:
|
|
3004
|
+
: captureTab
|
|
3005
|
+
? CAPTURE_TURN_KICKOFF({
|
|
3006
|
+
sessionId: job.sessionId,
|
|
3007
|
+
sessionName: job.sessionName,
|
|
3008
|
+
message,
|
|
3009
|
+
askedByName: job.askedByName,
|
|
3010
|
+
})
|
|
3011
|
+
: WORK_TURN_KICKOFF({
|
|
2995
3012
|
sessionId: job.sessionId,
|
|
2996
3013
|
sessionName: job.sessionName,
|
|
2997
3014
|
message,
|
|
2998
3015
|
askedByName: job.askedByName,
|
|
2999
3016
|
}),
|
|
3017
|
+
planPerm: captureTab,
|
|
3000
3018
|
// The adopt turn resumes the TERMINAL conversation by forking it
|
|
3001
3019
|
// into this cwd (claude: --resume <id> --fork-session). After it
|
|
3002
3020
|
// speaks once, the fork lives natively here and turn 2+ is the
|
|
3003
3021
|
// ordinary --continue resume path, unchanged.
|
|
3004
3022
|
...(adopting ? { adoptResumeId: job.adopt.id } : {}),
|
|
3005
|
-
system: plainTab ? SYSTEM_WORK_PLAIN : SYSTEM_WORK,
|
|
3023
|
+
system: plainTab ? SYSTEM_WORK_PLAIN : captureTab ? SYSTEM_CAPTURE : SYSTEM_WORK,
|
|
3006
3024
|
// Present only when the tab named one — see brainFor.
|
|
3007
3025
|
...brain,
|
|
3008
3026
|
// The tab watches the CLI work. Claude needs the flag to speak
|
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.82.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"
|