flowviant 0.69.0 → 0.70.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/localSessions.mjs +41 -0
- package/bin/lib/work.mjs +52 -6
- package/package.json +1 -1
|
@@ -74,6 +74,47 @@ function transcriptTitle(file, mtimeMs) {
|
|
|
74
74
|
return title;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* THE TITLE CLAUDE GAVE ITS OWN CONVERSATION, for one session we already know
|
|
79
|
+
* the id of.
|
|
80
|
+
*
|
|
81
|
+
* `scanLocalSessions` finds transcripts the hard way — walk every project
|
|
82
|
+
* directory, read each file's own `cwd` — because it is answering "what is in
|
|
83
|
+
* this repo" and knows no ids. This asks the same store a narrower question:
|
|
84
|
+
* the tab pinned the id the CLI reported at `system.init`, and the directory is
|
|
85
|
+
* the place the turn was spawned in, so the file is one munge away (Claude Code
|
|
86
|
+
* names the directory after the cwd with `/` and `.` replaced by `-`).
|
|
87
|
+
*
|
|
88
|
+
* The realpath fallback is not defensive padding: a place under a symlinked
|
|
89
|
+
* home munges to a DIFFERENT directory name depending on which form the CLI was
|
|
90
|
+
* handed, and the daemon does not control which that was.
|
|
91
|
+
*
|
|
92
|
+
* Returns null for anything it cannot read. A missing title is honest silence —
|
|
93
|
+
* the tab keeps whatever name it has.
|
|
94
|
+
*/
|
|
95
|
+
export function titleForSession(cwd, sessionId) {
|
|
96
|
+
if (!cwd || typeof sessionId !== 'string' || !/^[A-Za-z0-9_-]{8,64}$/.test(sessionId)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const projects = join(homedir(), '.claude', 'projects');
|
|
100
|
+
const dirs = [String(cwd)];
|
|
101
|
+
try {
|
|
102
|
+
const real = realpathSync(String(cwd));
|
|
103
|
+
if (real !== String(cwd)) dirs.push(real);
|
|
104
|
+
} catch {
|
|
105
|
+
/* the place is gone — nothing to read */
|
|
106
|
+
}
|
|
107
|
+
for (const dir of dirs) {
|
|
108
|
+
const file = join(projects, dir.replace(/[/.]/g, '-'), `${sessionId}.jsonl`);
|
|
109
|
+
try {
|
|
110
|
+
return transcriptTitle(file, statSync(file).mtimeMs);
|
|
111
|
+
} catch {
|
|
112
|
+
/* not this munge — try the other */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
77
118
|
/** Path-prefix containment on already-realpath'd absolute paths. */
|
|
78
119
|
const inside = (p, root) => p === root || p.startsWith(root.endsWith('/') ? root : `${root}/`);
|
|
79
120
|
|
package/bin/lib/work.mjs
CHANGED
|
@@ -59,7 +59,11 @@ import { detectRuntimes, canRun, recordSkills, RUNTIMES } from './runtimes.mjs';
|
|
|
59
59
|
/** The place id meaning "the checkout", not a worktree. Must match the
|
|
60
60
|
* server's REPO_PLACE — it is a wire value, not a local convention. */
|
|
61
61
|
const REPO_PLACE = 'repo';
|
|
62
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
isTerminalSessionLive,
|
|
64
|
+
isAgyConversationLive,
|
|
65
|
+
titleForSession,
|
|
66
|
+
} from './localSessions.mjs';
|
|
63
67
|
import { worktreeDiff } from './worktreeDiff.mjs';
|
|
64
68
|
import { homedir } from 'node:os';
|
|
65
69
|
|
|
@@ -398,10 +402,21 @@ export function createWorkManager({ repoRoot, baseDir, getBaseRef, getMcpUrl, ge
|
|
|
398
402
|
const learnPlaces = (map) => {
|
|
399
403
|
if (!map || typeof map !== 'object') return;
|
|
400
404
|
for (const [sid, place] of Object.entries(map)) {
|
|
401
|
-
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
+
// VALIDATE AT THE TRUST BOUNDARY. `placeDir` joins this value straight
|
|
406
|
+
// into `sessions/<place>` and it is the directory a turn is SPAWNED in
|
|
407
|
+
// and a preview port is measured against — the security boundary for the
|
|
408
|
+
// whole preview feature. The server resolves place to an enum (never a
|
|
409
|
+
// path), but a server bug or compromise sending `../../etc` would
|
|
410
|
+
// otherwise point a session's measured directory anywhere on the box and
|
|
411
|
+
// defeat the port attribution. `sessionWorktreeReport` and `placeWtFor`
|
|
412
|
+
// already reject an unsafe segment; doing it here covers every consumer
|
|
413
|
+
// (the preview-claim `placeDir` did not re-check). REPO_PLACE resolves to
|
|
414
|
+
// repoRoot, so it is allowed through despite not being a path segment.
|
|
415
|
+
if (typeof place === 'string' && place && (place === REPO_PLACE || isSafePathSegment(place)))
|
|
416
|
+
sessionPlaces.set(sid, place);
|
|
417
|
+
// null / '' / an unsafe value: the server is telling us this tab is in its
|
|
418
|
+
// OWN worktree (or is malformed). Falling back to the default requires
|
|
419
|
+
// forgetting, not ignoring.
|
|
405
420
|
else sessionPlaces.delete(sid);
|
|
406
421
|
}
|
|
407
422
|
};
|
|
@@ -465,6 +480,29 @@ export function createWorkManager({ repoRoot, baseDir, getBaseRef, getMcpUrl, ge
|
|
|
465
480
|
// `processesSupported` keeps "cannot look" (Windows) apart from "looked and
|
|
466
481
|
// found none", which renders differently.
|
|
467
482
|
const processes = sessionProcesses(sessionId);
|
|
483
|
+
// THE NAME CLAUDE ALREADY GAVE THIS CONVERSATION, relayed.
|
|
484
|
+
//
|
|
485
|
+
// Claude Code titles its own sessions; a Flowviant tab was born "session 3"
|
|
486
|
+
// and stayed that way unless somebody renamed it by hand, so a strip of
|
|
487
|
+
// eight tabs said nothing about any of them. The title is not ours to
|
|
488
|
+
// invent — reading it is the same relay this whole file does, and asking
|
|
489
|
+
// a model for one would be a second brain, which the product forbids.
|
|
490
|
+
//
|
|
491
|
+
// Only CLAUDE tabs have one here: the id is the one the CLI reported at
|
|
492
|
+
// `system.init` and pinned per tab, so a codex or agy tab simply has no
|
|
493
|
+
// marker and reports no title. No runtime check is needed for that — the
|
|
494
|
+
// absent marker IS the check.
|
|
495
|
+
//
|
|
496
|
+
// No version floor: a daemon→server report on an endpoint that already
|
|
497
|
+
// exists, so an older daemon sends no key and the server leaves the name
|
|
498
|
+
// exactly as it found it.
|
|
499
|
+
let title = null;
|
|
500
|
+
try {
|
|
501
|
+
const marker = sessionMetaPath(wt, 'flowviant-claude-session', sessionId);
|
|
502
|
+
if (marker) title = titleForSession(wt, readFileSync(marker, 'utf8').trim());
|
|
503
|
+
} catch {
|
|
504
|
+
/* no marker yet — this tab has not spoken, or is not Claude */
|
|
505
|
+
}
|
|
468
506
|
return {
|
|
469
507
|
sessionId,
|
|
470
508
|
...d,
|
|
@@ -472,6 +510,7 @@ export function createWorkManager({ repoRoot, baseDir, getBaseRef, getMcpUrl, ge
|
|
|
472
510
|
listeningSupported: listenersSupported(),
|
|
473
511
|
...(processes === null ? {} : { processes }),
|
|
474
512
|
processesSupported: processesSupported(),
|
|
513
|
+
...(title ? { title } : {}),
|
|
475
514
|
};
|
|
476
515
|
};
|
|
477
516
|
/** One session, now — called after its turn settles. */
|
|
@@ -1977,7 +2016,14 @@ export function createWorkManager({ repoRoot, baseDir, getBaseRef, getMcpUrl, ge
|
|
|
1977
2016
|
};
|
|
1978
2017
|
const auditCommand = (a) => {
|
|
1979
2018
|
if (a?.kind !== 'bash' || !a.command) return;
|
|
1980
|
-
|
|
2019
|
+
// Scrubbed like every other string that leaves this box (the
|
|
2020
|
+
// narrator label, the settle answer, commit subjects, ship/merge
|
|
2021
|
+
// lines, the per-tab process report). A command line is exactly
|
|
2022
|
+
// where a secret leaks — `curl -H "authorization: <token>"`,
|
|
2023
|
+
// `PGPASSWORD=… psql` — and the audit is stored 30 days and rendered
|
|
2024
|
+
// in the admin view, so the one uplink that omitted scrub was the
|
|
2025
|
+
// one most likely to carry a plaintext secret.
|
|
2026
|
+
auditBatch.push({ command: envScrub(a.command), at: new Date().toISOString() });
|
|
1981
2027
|
if (auditBatch.length >= 25) flushAudit();
|
|
1982
2028
|
};
|
|
1983
2029
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant — 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": {
|