@phnx-labs/agents-cli 1.20.28 → 1.20.30
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/dist/commands/computer-actions.js +6 -2
- package/dist/commands/computer.d.ts +12 -0
- package/dist/commands/computer.js +88 -13
- package/dist/commands/exec.js +22 -10
- package/dist/commands/inspect.js +1 -1
- package/dist/commands/models.js +8 -2
- package/dist/commands/secrets.js +93 -6
- package/dist/commands/sessions.js +157 -44
- package/dist/commands/ssh.d.ts +14 -0
- package/dist/commands/ssh.js +263 -0
- package/dist/commands/sync.js +70 -14
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +54 -5
- package/dist/lib/browser/drivers/ssh.js +4 -35
- package/dist/lib/computer-rpc.d.ts +6 -1
- package/dist/lib/computer-rpc.js +86 -3
- package/dist/lib/devices/connect.d.ts +34 -0
- package/dist/lib/devices/connect.js +101 -0
- package/dist/lib/devices/registry.d.ts +78 -0
- package/dist/lib/devices/registry.js +168 -0
- package/dist/lib/devices/ssh-config.d.ts +21 -0
- package/dist/lib/devices/ssh-config.js +33 -0
- package/dist/lib/devices/tailscale.d.ts +31 -0
- package/dist/lib/devices/tailscale.js +126 -0
- package/dist/lib/exec.js +14 -0
- package/dist/lib/models.js +138 -5
- package/dist/lib/runner.js +7 -7
- package/dist/lib/secrets/remote.d.ts +67 -0
- package/dist/lib/secrets/remote.js +133 -0
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/active.js +79 -18
- package/dist/lib/session/cloud.js +2 -0
- package/dist/lib/session/db.d.ts +12 -0
- package/dist/lib/session/db.js +66 -9
- package/dist/lib/session/discover.d.ts +7 -0
- package/dist/lib/session/discover.js +309 -0
- package/dist/lib/session/parse.d.ts +22 -0
- package/dist/lib/session/parse.js +132 -2
- package/dist/lib/session/remote.d.ts +1 -1
- package/dist/lib/session/remote.js +8 -3
- package/dist/lib/session/state.d.ts +82 -0
- package/dist/lib/session/state.js +221 -0
- package/dist/lib/session/tail.d.ts +18 -0
- package/dist/lib/session/tail.js +57 -0
- package/dist/lib/session/types.d.ts +10 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +29 -0
- package/dist/lib/session/width.js +91 -0
- package/dist/lib/shims.d.ts +17 -1
- package/dist/lib/shims.js +130 -6
- package/dist/lib/ssh-tunnel.d.ts +127 -0
- package/dist/lib/ssh-tunnel.js +346 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +4 -0
- package/dist/lib/state.js +19 -1
- package/dist/lib/teams/agents.d.ts +11 -1
- package/dist/lib/teams/agents.js +16 -2
- package/dist/lib/types.d.ts +1 -0
- package/dist/lib/versions.d.ts +19 -0
- package/dist/lib/versions.js +84 -24
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session state inference.
|
|
3
|
+
*
|
|
4
|
+
* Turns a chronological slice of normalized `SessionEvent`s (typically the tail
|
|
5
|
+
* of a transcript) plus lightweight context (file mtime, cwd, branch, whether
|
|
6
|
+
* the owning process is alive) into a `SessionState`: is the agent working,
|
|
7
|
+
* waiting on the user, or idle — and did it open a PR, is it in a worktree, is
|
|
8
|
+
* it tied to a tracker ticket. Pure functions, no I/O, so the whole thing is
|
|
9
|
+
* unit-testable and shared by both the live `--active` path and the incremental
|
|
10
|
+
* scanner (which persists the durable signals to the index).
|
|
11
|
+
*
|
|
12
|
+
* Structural signals are preferred over prose heuristics: Claude's
|
|
13
|
+
* `ExitPlanMode` / `AskUserQuestion` tool calls are exact "waiting on you"
|
|
14
|
+
* markers. Codex has no such tools, so it falls back to last-role + question
|
|
15
|
+
* shape + mtime — same function, driven off the normalized events.
|
|
16
|
+
*/
|
|
17
|
+
import type { SessionEvent } from './types.js';
|
|
18
|
+
export type SessionActivity = 'working' | 'waiting_input' | 'idle';
|
|
19
|
+
export type AwaitingReason = 'question' | 'plan_review' | 'permission';
|
|
20
|
+
export interface DetectedPr {
|
|
21
|
+
url: string;
|
|
22
|
+
number?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface DetectedWorktree {
|
|
25
|
+
/** Absolute worktree path (the session cwd). */
|
|
26
|
+
path: string;
|
|
27
|
+
/** The `<slug>` under `.agents/worktrees/`. */
|
|
28
|
+
slug: string;
|
|
29
|
+
branch?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DetectedTicket {
|
|
32
|
+
/** Tracker key, e.g. `RUSH-1234`. */
|
|
33
|
+
id: string;
|
|
34
|
+
url?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface SessionState {
|
|
37
|
+
activity: SessionActivity;
|
|
38
|
+
awaitingReason?: AwaitingReason;
|
|
39
|
+
lastRole?: 'user' | 'assistant';
|
|
40
|
+
lastEventKind?: SessionEvent['type'];
|
|
41
|
+
/** Single-line description of the latest turn (message text or tool action). */
|
|
42
|
+
preview?: string;
|
|
43
|
+
lastActivityMs?: number;
|
|
44
|
+
pr?: DetectedPr;
|
|
45
|
+
worktree?: DetectedWorktree;
|
|
46
|
+
ticket?: DetectedTicket;
|
|
47
|
+
}
|
|
48
|
+
export interface StateContext {
|
|
49
|
+
/** Session file mtime; drives running-vs-stale. */
|
|
50
|
+
mtimeMs?: number;
|
|
51
|
+
cwd?: string;
|
|
52
|
+
gitBranch?: string;
|
|
53
|
+
/** Whether the owning OS process is alive (from the active scanner). */
|
|
54
|
+
pidAlive?: boolean;
|
|
55
|
+
/** Override the running window (defaults to 2 min, matching active.ts). */
|
|
56
|
+
activeWindowMs?: number;
|
|
57
|
+
}
|
|
58
|
+
/** Detect a worktree from the session cwd, per the `.agents/worktrees/<slug>/` convention. */
|
|
59
|
+
export declare function detectWorktree(cwd?: string, branch?: string): DetectedWorktree | undefined;
|
|
60
|
+
/** Detect a tracker ticket from free text (prompt/topic) then a branch name. */
|
|
61
|
+
export declare function detectTicket(text?: string, branch?: string): DetectedTicket | undefined;
|
|
62
|
+
/** Pull a PR URL + number out of tool-result output text. */
|
|
63
|
+
export declare function extractPrUrl(output?: string): DetectedPr | undefined;
|
|
64
|
+
/** True when a Bash/exec command string is a `gh pr create`. */
|
|
65
|
+
export declare function isPrCreateCommand(command?: string): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Infer live activity + a preview from a chronological event slice. `pr` /
|
|
68
|
+
* `ticket` / `worktree` are attached by `inferSessionState`; this focuses on the
|
|
69
|
+
* running-vs-waiting-vs-idle decision and the preview line.
|
|
70
|
+
*/
|
|
71
|
+
export declare function inferActivity(events: SessionEvent[], ctx?: StateContext): SessionState;
|
|
72
|
+
/**
|
|
73
|
+
* Scan an event slice for the durable signals (PR opened, ticket) that aren't
|
|
74
|
+
* about the cwd. Correlates each `gh pr create` with the nearest following
|
|
75
|
+
* tool_result URL; keeps the last PR found.
|
|
76
|
+
*/
|
|
77
|
+
export declare function detectDurableSignals(events: SessionEvent[]): {
|
|
78
|
+
pr?: DetectedPr;
|
|
79
|
+
ticket?: DetectedTicket;
|
|
80
|
+
};
|
|
81
|
+
/** Full inference: activity + preview + durable signals + worktree/ticket from ctx. */
|
|
82
|
+
export declare function inferSessionState(events: SessionEvent[], ctx?: StateContext): SessionState;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session state inference.
|
|
3
|
+
*
|
|
4
|
+
* Turns a chronological slice of normalized `SessionEvent`s (typically the tail
|
|
5
|
+
* of a transcript) plus lightweight context (file mtime, cwd, branch, whether
|
|
6
|
+
* the owning process is alive) into a `SessionState`: is the agent working,
|
|
7
|
+
* waiting on the user, or idle — and did it open a PR, is it in a worktree, is
|
|
8
|
+
* it tied to a tracker ticket. Pure functions, no I/O, so the whole thing is
|
|
9
|
+
* unit-testable and shared by both the live `--active` path and the incremental
|
|
10
|
+
* scanner (which persists the durable signals to the index).
|
|
11
|
+
*
|
|
12
|
+
* Structural signals are preferred over prose heuristics: Claude's
|
|
13
|
+
* `ExitPlanMode` / `AskUserQuestion` tool calls are exact "waiting on you"
|
|
14
|
+
* markers. Codex has no such tools, so it falls back to last-role + question
|
|
15
|
+
* shape + mtime — same function, driven off the normalized events.
|
|
16
|
+
*/
|
|
17
|
+
import { summarizeToolUse } from './parse.js';
|
|
18
|
+
/** A healthy live session writes several times a minute; 2 min ⇒ "recently active". */
|
|
19
|
+
const ACTIVE_WINDOW_MS = 2 * 60_000;
|
|
20
|
+
/** Claude tool names that structurally mean "the agent handed control back to you". */
|
|
21
|
+
const PLAN_TOOL = 'ExitPlanMode';
|
|
22
|
+
const ASK_TOOL = 'AskUserQuestion';
|
|
23
|
+
/** Trailing '?' or a leading interrogative — a question aimed at the user. */
|
|
24
|
+
const QUESTION_TRAILING = /\?["'”)\]]?\s*$/;
|
|
25
|
+
const QUESTION_PHRASE = /\b(shall i|should i|do you want|would you like|which (?:one|option|approach|of)|can you (?:confirm|clarify)|please (?:confirm|clarify|advise)|let me know|are you (?:ok|okay|sure)|proceed\?)\b/i;
|
|
26
|
+
/**
|
|
27
|
+
* Linear/Jira-style ref, e.g. RUSH-1234. Team key is letters-only (2–6) so a
|
|
28
|
+
* regex snippet like `[A-Z0-9]-\d` in a code discussion can't masquerade as a
|
|
29
|
+
* ticket. Uppercase-only so we don't match `utf-8`.
|
|
30
|
+
*/
|
|
31
|
+
const TICKET_RE = /\b([A-Z]{2,6}-\d{1,6})\b/;
|
|
32
|
+
/** Lowercase branch form (Linear branch names): muqsit/rush-1234-fix. */
|
|
33
|
+
const TICKET_BRANCH_RE = /(?:^|[/_-])([a-z]{2,6})-(\d{2,6})(?=[/_-]|$)/;
|
|
34
|
+
/** Keys that look like tickets but aren't — avoid false positives from branches. */
|
|
35
|
+
const TICKET_DENYLIST = new Set(['UTF', 'SHA', 'ISO', 'RFC', 'IPV', 'X86', 'ARM', 'MP', 'H']);
|
|
36
|
+
const PR_URL_RE = /https:\/\/github\.com\/[^\s"'()<>]+\/pull\/(\d+)/;
|
|
37
|
+
const WORKTREE_RE = /\/\.agents\/worktrees\/([^/]+)/;
|
|
38
|
+
/** gh invocations that create/open a PR. */
|
|
39
|
+
const GH_PR_CREATE_RE = /\bgh\s+pr\s+(?:create|new)\b/;
|
|
40
|
+
/** Collapse to a single trimmed line for a one-row preview cell. */
|
|
41
|
+
function oneLine(s) {
|
|
42
|
+
return s.replace(/\s+/g, ' ').trim();
|
|
43
|
+
}
|
|
44
|
+
/** Detect a worktree from the session cwd, per the `.agents/worktrees/<slug>/` convention. */
|
|
45
|
+
export function detectWorktree(cwd, branch) {
|
|
46
|
+
if (!cwd)
|
|
47
|
+
return undefined;
|
|
48
|
+
const m = cwd.match(WORKTREE_RE);
|
|
49
|
+
if (!m)
|
|
50
|
+
return undefined;
|
|
51
|
+
return { path: cwd, slug: m[1], branch: branch || undefined };
|
|
52
|
+
}
|
|
53
|
+
/** Detect a tracker ticket from free text (prompt/topic) then a branch name. */
|
|
54
|
+
export function detectTicket(text, branch) {
|
|
55
|
+
if (text) {
|
|
56
|
+
const m = text.match(TICKET_RE);
|
|
57
|
+
if (m && !TICKET_DENYLIST.has(m[1].split('-')[0]))
|
|
58
|
+
return { id: m[1] };
|
|
59
|
+
}
|
|
60
|
+
if (branch) {
|
|
61
|
+
const m = branch.match(TICKET_BRANCH_RE);
|
|
62
|
+
if (m) {
|
|
63
|
+
const key = m[1].toUpperCase();
|
|
64
|
+
if (!TICKET_DENYLIST.has(key))
|
|
65
|
+
return { id: `${key}-${m[2]}` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
/** Pull a PR URL + number out of tool-result output text. */
|
|
71
|
+
export function extractPrUrl(output) {
|
|
72
|
+
if (!output)
|
|
73
|
+
return undefined;
|
|
74
|
+
const m = output.match(PR_URL_RE);
|
|
75
|
+
if (!m)
|
|
76
|
+
return undefined;
|
|
77
|
+
return { url: m[0], number: Number.parseInt(m[1], 10) };
|
|
78
|
+
}
|
|
79
|
+
/** True when a Bash/exec command string is a `gh pr create`. */
|
|
80
|
+
export function isPrCreateCommand(command) {
|
|
81
|
+
return !!command && GH_PR_CREATE_RE.test(command);
|
|
82
|
+
}
|
|
83
|
+
/** Does an assistant message read as a question directed at the user? */
|
|
84
|
+
function looksLikeQuestion(text) {
|
|
85
|
+
const t = text.trim();
|
|
86
|
+
if (!t)
|
|
87
|
+
return false;
|
|
88
|
+
// Only weigh the final line — a long answer that ends with a question is a question.
|
|
89
|
+
const lastLine = t.split('\n').filter(Boolean).pop() ?? t;
|
|
90
|
+
return QUESTION_TRAILING.test(lastLine) || QUESTION_PHRASE.test(lastLine);
|
|
91
|
+
}
|
|
92
|
+
/** Human-readable one-liner for the latest event (message text or tool action). */
|
|
93
|
+
function describeEvent(e) {
|
|
94
|
+
if (e.type === 'message' && e.content)
|
|
95
|
+
return oneLine(e.content);
|
|
96
|
+
if (e.type === 'tool_use' && e.tool)
|
|
97
|
+
return oneLine(summarizeToolUse(e.tool, e.args));
|
|
98
|
+
if (e.type === 'thinking')
|
|
99
|
+
return 'thinking…';
|
|
100
|
+
if (e.type === 'tool_result')
|
|
101
|
+
return e.tool ? `↳ ${e.tool}` : undefined;
|
|
102
|
+
if (e.type === 'error')
|
|
103
|
+
return oneLine(e.content || 'error');
|
|
104
|
+
return e.content ? oneLine(e.content) : undefined;
|
|
105
|
+
}
|
|
106
|
+
/** Last event of a given type, scanning from the end. */
|
|
107
|
+
function lastOf(events, pred) {
|
|
108
|
+
for (let i = events.length - 1; i >= 0; i--)
|
|
109
|
+
if (pred(events[i]))
|
|
110
|
+
return events[i];
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Infer live activity + a preview from a chronological event slice. `pr` /
|
|
115
|
+
* `ticket` / `worktree` are attached by `inferSessionState`; this focuses on the
|
|
116
|
+
* running-vs-waiting-vs-idle decision and the preview line.
|
|
117
|
+
*/
|
|
118
|
+
export function inferActivity(events, ctx = {}) {
|
|
119
|
+
const windowMs = ctx.activeWindowMs ?? ACTIVE_WINDOW_MS;
|
|
120
|
+
const fresh = ctx.mtimeMs != null && Date.now() - ctx.mtimeMs < windowMs;
|
|
121
|
+
// A non-live process (pidAlive === false) can never be "working"; the strongest
|
|
122
|
+
// it gets is "waiting on you" (a dangling question) or "idle".
|
|
123
|
+
const canWork = ctx.pidAlive !== false && (ctx.pidAlive === true || fresh);
|
|
124
|
+
const meaningful = events.filter(e => e.type === 'message' || e.type === 'tool_use' || e.type === 'tool_result' || e.type === 'thinking' || e.type === 'error');
|
|
125
|
+
const last = meaningful[meaningful.length - 1];
|
|
126
|
+
const lastMsg = lastOf(meaningful, e => e.type === 'message');
|
|
127
|
+
const lastToolUse = lastOf(meaningful, e => e.type === 'tool_use');
|
|
128
|
+
// The most informative recent line: a message or tool call as-is, but for a
|
|
129
|
+
// trailing tool_result/thinking show the tool *call* that produced it (its
|
|
130
|
+
// command) rather than a bare "↳ Bash".
|
|
131
|
+
const previewSource = !last
|
|
132
|
+
? undefined
|
|
133
|
+
: last.type === 'message' || last.type === 'tool_use'
|
|
134
|
+
? last
|
|
135
|
+
: (lastToolUse ?? last);
|
|
136
|
+
const base = {
|
|
137
|
+
activity: 'idle',
|
|
138
|
+
lastRole: lastMsg?.role,
|
|
139
|
+
lastEventKind: last?.type,
|
|
140
|
+
lastActivityMs: ctx.mtimeMs,
|
|
141
|
+
preview: previewSource ? describeEvent(previewSource) : undefined,
|
|
142
|
+
};
|
|
143
|
+
if (!last)
|
|
144
|
+
return base;
|
|
145
|
+
// Structural "waiting on you" — Claude handed control back via a plan/question
|
|
146
|
+
// tool and nothing has come after it.
|
|
147
|
+
const lastPlanOrAsk = lastOf(meaningful, e => e.type === 'tool_use' && (e.tool === PLAN_TOOL || e.tool === ASK_TOOL));
|
|
148
|
+
if (lastPlanOrAsk && meaningful.indexOf(lastPlanOrAsk) === meaningful.length - 1) {
|
|
149
|
+
return {
|
|
150
|
+
...base,
|
|
151
|
+
activity: 'waiting_input',
|
|
152
|
+
awaitingReason: lastPlanOrAsk.tool === PLAN_TOOL ? 'plan_review' : 'question',
|
|
153
|
+
preview: lastPlanOrAsk.tool === PLAN_TOOL ? 'Plan ready — awaiting your review' : 'Asked you a question',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Pending tool call (tool_use with no following tool_result): mid-turn.
|
|
157
|
+
if (last.type === 'tool_use') {
|
|
158
|
+
if (canWork && fresh)
|
|
159
|
+
return { ...base, activity: 'working' };
|
|
160
|
+
// Alive but the file hasn't moved — likely blocked on a permission prompt.
|
|
161
|
+
if (ctx.pidAlive)
|
|
162
|
+
return { ...base, activity: 'waiting_input', awaitingReason: 'permission' };
|
|
163
|
+
return { ...base, activity: 'idle' };
|
|
164
|
+
}
|
|
165
|
+
// Thinking or a tool result just landed → agent is mid-turn if recently active.
|
|
166
|
+
if (last.type === 'thinking' || last.type === 'tool_result' || last.type === 'error') {
|
|
167
|
+
return { ...base, activity: canWork && fresh ? 'working' : 'idle' };
|
|
168
|
+
}
|
|
169
|
+
// Last event is a message.
|
|
170
|
+
if (last.type === 'message') {
|
|
171
|
+
if (last.role === 'user') {
|
|
172
|
+
// User spoke last; the agent owes a reply → working if it's alive/fresh.
|
|
173
|
+
return { ...base, activity: canWork ? 'working' : 'idle' };
|
|
174
|
+
}
|
|
175
|
+
// Assistant spoke last and stopped. A trailing question → waiting; else idle.
|
|
176
|
+
if (looksLikeQuestion(last.content ?? '')) {
|
|
177
|
+
return { ...base, activity: 'waiting_input', awaitingReason: 'question' };
|
|
178
|
+
}
|
|
179
|
+
return { ...base, activity: 'idle' };
|
|
180
|
+
}
|
|
181
|
+
return base;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Scan an event slice for the durable signals (PR opened, ticket) that aren't
|
|
185
|
+
* about the cwd. Correlates each `gh pr create` with the nearest following
|
|
186
|
+
* tool_result URL; keeps the last PR found.
|
|
187
|
+
*/
|
|
188
|
+
export function detectDurableSignals(events) {
|
|
189
|
+
let pr;
|
|
190
|
+
let sawPrCreate = false;
|
|
191
|
+
let ticket;
|
|
192
|
+
for (const e of events) {
|
|
193
|
+
// Structural PR signal: a real `gh pr create` tool call, then the pull URL
|
|
194
|
+
// from a following tool_result — never a bare URL mentioned in prose.
|
|
195
|
+
if (e.type === 'tool_use' && isPrCreateCommand(e.command))
|
|
196
|
+
sawPrCreate = true;
|
|
197
|
+
if (sawPrCreate && e.type === 'tool_result') {
|
|
198
|
+
const found = extractPrUrl(e.output);
|
|
199
|
+
if (found) {
|
|
200
|
+
pr = found;
|
|
201
|
+
sawPrCreate = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!ticket && e.type === 'message' && e.role === 'user') {
|
|
205
|
+
ticket = detectTicket(e.content);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { pr, ticket };
|
|
209
|
+
}
|
|
210
|
+
/** Full inference: activity + preview + durable signals + worktree/ticket from ctx. */
|
|
211
|
+
export function inferSessionState(events, ctx = {}) {
|
|
212
|
+
const state = inferActivity(events, ctx);
|
|
213
|
+
const { pr, ticket } = detectDurableSignals(events);
|
|
214
|
+
const worktree = detectWorktree(ctx.cwd, ctx.gitBranch);
|
|
215
|
+
return {
|
|
216
|
+
...state,
|
|
217
|
+
pr: pr ?? state.pr,
|
|
218
|
+
worktree: worktree ?? state.worktree,
|
|
219
|
+
ticket: ticket ?? detectTicket(undefined, ctx.gitBranch) ?? state.ticket,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast tail read of a session transcript.
|
|
3
|
+
*
|
|
4
|
+
* The live `--active` view needs the *last* few events of a possibly-huge JSONL
|
|
5
|
+
* to infer state — parsing the whole file per row would make the view crawl. We
|
|
6
|
+
* read only the final chunk from an fd (mirroring the bounded head-read in
|
|
7
|
+
* active.ts's `quickExtractTopic`), drop a partial leading line, and hand the
|
|
8
|
+
* chunk to the existing content parsers so there's zero duplicated parse logic.
|
|
9
|
+
*/
|
|
10
|
+
import type { SessionAgentId, SessionEvent } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Read the last `maxBytes` of a JSONL transcript and return its last
|
|
13
|
+
* `maxEvents` normalized events. A tail that begins mid-line yields one
|
|
14
|
+
* malformed leading line, which the per-line JSON try/catch in the content
|
|
15
|
+
* parsers skips. Only Claude and Codex are supported (the prioritized harnesses
|
|
16
|
+
* for live state); other agents return `[]`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readSessionTail(filePath: string, agent: SessionAgentId, maxBytes?: number, maxEvents?: number): SessionEvent[];
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast tail read of a session transcript.
|
|
3
|
+
*
|
|
4
|
+
* The live `--active` view needs the *last* few events of a possibly-huge JSONL
|
|
5
|
+
* to infer state — parsing the whole file per row would make the view crawl. We
|
|
6
|
+
* read only the final chunk from an fd (mirroring the bounded head-read in
|
|
7
|
+
* active.ts's `quickExtractTopic`), drop a partial leading line, and hand the
|
|
8
|
+
* chunk to the existing content parsers so there's zero duplicated parse logic.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import { parseClaudeContent, parseCodexContent, sanitizeEvents } from './parse.js';
|
|
12
|
+
const DEFAULT_MAX_BYTES = 128 * 1024;
|
|
13
|
+
const DEFAULT_MAX_EVENTS = 60;
|
|
14
|
+
/**
|
|
15
|
+
* Read the last `maxBytes` of a JSONL transcript and return its last
|
|
16
|
+
* `maxEvents` normalized events. A tail that begins mid-line yields one
|
|
17
|
+
* malformed leading line, which the per-line JSON try/catch in the content
|
|
18
|
+
* parsers skips. Only Claude and Codex are supported (the prioritized harnesses
|
|
19
|
+
* for live state); other agents return `[]`.
|
|
20
|
+
*/
|
|
21
|
+
export function readSessionTail(filePath, agent, maxBytes = DEFAULT_MAX_BYTES, maxEvents = DEFAULT_MAX_EVENTS) {
|
|
22
|
+
if (agent !== 'claude' && agent !== 'codex')
|
|
23
|
+
return [];
|
|
24
|
+
let fd;
|
|
25
|
+
try {
|
|
26
|
+
fd = fs.openSync(filePath, 'r');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const size = fs.fstatSync(fd).size;
|
|
33
|
+
if (size === 0)
|
|
34
|
+
return [];
|
|
35
|
+
const start = Math.max(0, size - maxBytes);
|
|
36
|
+
const len = size - start;
|
|
37
|
+
const buf = Buffer.alloc(len);
|
|
38
|
+
fs.readSync(fd, buf, 0, len, start);
|
|
39
|
+
let content = buf.toString('utf8');
|
|
40
|
+
// If we started mid-file, the first line is almost certainly partial — drop it.
|
|
41
|
+
if (start > 0) {
|
|
42
|
+
const nl = content.indexOf('\n');
|
|
43
|
+
content = nl >= 0 ? content.slice(nl + 1) : '';
|
|
44
|
+
}
|
|
45
|
+
if (!content.trim())
|
|
46
|
+
return [];
|
|
47
|
+
const events = agent === 'codex' ? parseCodexContent(content) : parseClaudeContent(content);
|
|
48
|
+
sanitizeEvents(events);
|
|
49
|
+
return events.length > maxEvents ? events.slice(-maxEvents) : events;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
fs.closeSync(fd);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** Agents that store session data on disk and can be discovered by `agents sessions`. */
|
|
10
|
-
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi';
|
|
10
|
+
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid';
|
|
11
11
|
/** All agents with session discovery support, in display order. */
|
|
12
12
|
export declare const SESSION_AGENTS: SessionAgentId[];
|
|
13
13
|
/** A single normalized event within a session (message, tool call, thinking, etc.). */
|
|
@@ -63,6 +63,15 @@ export interface SessionMeta {
|
|
|
63
63
|
label?: string;
|
|
64
64
|
/** Set when this session was spawned by `agents teams`. */
|
|
65
65
|
teamOrigin?: TeamOrigin;
|
|
66
|
+
/** Durable state signals extracted at scan time by the session-state engine. */
|
|
67
|
+
/** PR URL, if the session opened one (`gh pr create`). */
|
|
68
|
+
prUrl?: string;
|
|
69
|
+
/** PR number parsed from prUrl, for compact display. */
|
|
70
|
+
prNumber?: number;
|
|
71
|
+
/** Worktree slug when cwd is under `.agents/worktrees/<slug>/`. */
|
|
72
|
+
worktreeSlug?: string;
|
|
73
|
+
/** Tracker ticket ref (e.g. RUSH-1234) from the prompt or branch. */
|
|
74
|
+
ticketId?: string;
|
|
66
75
|
/**
|
|
67
76
|
* True when the session was spawned programmatically (SDK entrypoint) rather
|
|
68
77
|
* than by a human at the Claude CLI. Captured at scan time from the JSONL
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** All agents with session discovery support, in display order. */
|
|
10
|
-
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi'];
|
|
10
|
+
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid'];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal display-width helpers.
|
|
3
|
+
*
|
|
4
|
+
* `String.length` is the wrong ruler for a terminal: it over-counts ANSI colour
|
|
5
|
+
* escapes (chalk output) and under-counts wide glyphs (CJK, emoji) which occupy
|
|
6
|
+
* two cells. The result is the drifting, wrapping session-table line users see
|
|
7
|
+
* under tmux and over `--host` SSH. Every renderer that sizes a session-table
|
|
8
|
+
* cell measures and truncates through this module so alignment is computed once,
|
|
9
|
+
* correctly, from the same source of truth.
|
|
10
|
+
*/
|
|
11
|
+
/** Strip SGR colour escapes so width is measured on visible characters only. */
|
|
12
|
+
export declare function stripAnsi(s: string): string;
|
|
13
|
+
/** Visible display width of a string, ANSI-aware and wide-char-aware. */
|
|
14
|
+
export declare function stringWidth(s: string): number;
|
|
15
|
+
/**
|
|
16
|
+
* Truncate to a target display width, appending '…' when shortened. Operates on
|
|
17
|
+
* the visible (ANSI-stripped) string; callers colour the result afterwards so
|
|
18
|
+
* the ellipsis is never inserted mid-escape.
|
|
19
|
+
*/
|
|
20
|
+
export declare function truncateToWidth(s: string, max: number): string;
|
|
21
|
+
/** Right-pad with spaces to a target display width. Never truncates. */
|
|
22
|
+
export declare function padToWidth(s: string, width: number): string;
|
|
23
|
+
/**
|
|
24
|
+
* Effective terminal width. Reads `$COLUMNS` first so it survives tmux and
|
|
25
|
+
* `--host` SSH (where `process.stdout.columns` is unset or wrong), falls back to
|
|
26
|
+
* the TTY's reported width, then to `fallback`. Clamped to a sane band so a
|
|
27
|
+
* bogus value can't produce a 0-wide or absurdly long table.
|
|
28
|
+
*/
|
|
29
|
+
export declare function terminalWidth(fallback?: number): number;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal display-width helpers.
|
|
3
|
+
*
|
|
4
|
+
* `String.length` is the wrong ruler for a terminal: it over-counts ANSI colour
|
|
5
|
+
* escapes (chalk output) and under-counts wide glyphs (CJK, emoji) which occupy
|
|
6
|
+
* two cells. The result is the drifting, wrapping session-table line users see
|
|
7
|
+
* under tmux and over `--host` SSH. Every renderer that sizes a session-table
|
|
8
|
+
* cell measures and truncates through this module so alignment is computed once,
|
|
9
|
+
* correctly, from the same source of truth.
|
|
10
|
+
*/
|
|
11
|
+
/** SGR colour sequences emitted by chalk (e.g. `\x1b[32m`). */
|
|
12
|
+
const SGR_REGEX = /\x1b\[[0-9;]*m/g;
|
|
13
|
+
/** Strip SGR colour escapes so width is measured on visible characters only. */
|
|
14
|
+
export function stripAnsi(s) {
|
|
15
|
+
return s.replace(SGR_REGEX, '');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Display cells for one code point: 0 for zero-width combining/ZWJ/variation
|
|
19
|
+
* selectors, 2 for East-Asian-wide and emoji ranges, 1 otherwise. Compact and
|
|
20
|
+
* dependency-free — covers the glyphs that actually show up in prompts/titles.
|
|
21
|
+
*/
|
|
22
|
+
function charWidth(cp) {
|
|
23
|
+
if (cp === 0)
|
|
24
|
+
return 0;
|
|
25
|
+
// Zero-width: combining marks, zero-width joiner, variation selectors.
|
|
26
|
+
if ((cp >= 0x0300 && cp <= 0x036f) ||
|
|
27
|
+
cp === 0x200b || cp === 0x200d ||
|
|
28
|
+
(cp >= 0xfe00 && cp <= 0xfe0f))
|
|
29
|
+
return 0;
|
|
30
|
+
// Wide (2 cells): CJK, Hangul, fullwidth forms, emoji & pictographs.
|
|
31
|
+
if ((cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
32
|
+
(cp >= 0x2e80 && cp <= 0xa4cf) || // CJK radicals … Yi
|
|
33
|
+
(cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
|
|
34
|
+
(cp >= 0xf900 && cp <= 0xfaff) || // CJK compatibility ideographs
|
|
35
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) || // CJK compatibility forms
|
|
36
|
+
(cp >= 0xff00 && cp <= 0xff60) || // Fullwidth forms
|
|
37
|
+
(cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth signs
|
|
38
|
+
(cp >= 0x1f300 && cp <= 0x1faff) || // emoji & symbols
|
|
39
|
+
(cp >= 0x20000 && cp <= 0x3fffd) // CJK Ext-B and beyond
|
|
40
|
+
)
|
|
41
|
+
return 2;
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
/** Visible display width of a string, ANSI-aware and wide-char-aware. */
|
|
45
|
+
export function stringWidth(s) {
|
|
46
|
+
const plain = stripAnsi(s);
|
|
47
|
+
let w = 0;
|
|
48
|
+
for (const ch of plain)
|
|
49
|
+
w += charWidth(ch.codePointAt(0));
|
|
50
|
+
return w;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Truncate to a target display width, appending '…' when shortened. Operates on
|
|
54
|
+
* the visible (ANSI-stripped) string; callers colour the result afterwards so
|
|
55
|
+
* the ellipsis is never inserted mid-escape.
|
|
56
|
+
*/
|
|
57
|
+
export function truncateToWidth(s, max) {
|
|
58
|
+
if (max <= 0)
|
|
59
|
+
return '';
|
|
60
|
+
const plain = stripAnsi(s);
|
|
61
|
+
if (stringWidth(plain) <= max)
|
|
62
|
+
return plain;
|
|
63
|
+
let w = 0;
|
|
64
|
+
let out = '';
|
|
65
|
+
for (const ch of plain) {
|
|
66
|
+
const cw = charWidth(ch.codePointAt(0));
|
|
67
|
+
if (w + cw > max - 1)
|
|
68
|
+
break; // reserve one cell for the ellipsis
|
|
69
|
+
out += ch;
|
|
70
|
+
w += cw;
|
|
71
|
+
}
|
|
72
|
+
return out + '…';
|
|
73
|
+
}
|
|
74
|
+
/** Right-pad with spaces to a target display width. Never truncates. */
|
|
75
|
+
export function padToWidth(s, width) {
|
|
76
|
+
const pad = width - stringWidth(s);
|
|
77
|
+
return pad > 0 ? s + ' '.repeat(pad) : s;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Effective terminal width. Reads `$COLUMNS` first so it survives tmux and
|
|
81
|
+
* `--host` SSH (where `process.stdout.columns` is unset or wrong), falls back to
|
|
82
|
+
* the TTY's reported width, then to `fallback`. Clamped to a sane band so a
|
|
83
|
+
* bogus value can't produce a 0-wide or absurdly long table.
|
|
84
|
+
*/
|
|
85
|
+
export function terminalWidth(fallback = 100) {
|
|
86
|
+
const env = Number.parseInt(process.env.COLUMNS ?? '', 10);
|
|
87
|
+
const raw = Number.isFinite(env) && env > 0
|
|
88
|
+
? env
|
|
89
|
+
: (process.stdout.columns || fallback);
|
|
90
|
+
return Math.max(60, Math.min(200, raw));
|
|
91
|
+
}
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -108,8 +108,14 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
108
108
|
* v5 — hard-disable Codex startup update checks in versioned aliases.
|
|
109
109
|
* v6 — versions moved from ~/.agents-system/versions to ~/.agents/versions
|
|
110
110
|
* (two-repo split: system = shipped defaults, user = operational state).
|
|
111
|
+
* v7 — runtime state split into ~/.agents/.history and ~/.agents/.cache.
|
|
112
|
+
* v8 — resolve grok/kimi/droid binaries from their real install locations
|
|
113
|
+
* (~/.grok/downloads, ~/.kimi-code/bin, ~/.local/bin) instead of the
|
|
114
|
+
* hardcoded node_modules/.bin, which never exists for these three and
|
|
115
|
+
* made every versioned alias (the path `agents teams` pins to) fail
|
|
116
|
+
* with "<agent>@<version> not installed". Also emit GROK_HOME.
|
|
111
117
|
*/
|
|
112
|
-
export declare const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
118
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 8;
|
|
113
119
|
/**
|
|
114
120
|
* Generate a versioned alias script that directly execs a specific version.
|
|
115
121
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -159,6 +165,16 @@ export declare function versionedAliasExists(agent: AgentId, version: string): b
|
|
|
159
165
|
*
|
|
160
166
|
* Returns: { success: boolean, backupPath?: string, error?: string }
|
|
161
167
|
*/
|
|
168
|
+
/**
|
|
169
|
+
* Seed a version's config home with the account credential so switching versions
|
|
170
|
+
* doesn't log the CLI out. Droid/antigravity/kimi (registry `authFiles`) store
|
|
171
|
+
* login as files inside the per-version config dir; sign-in is account-global,
|
|
172
|
+
* so we copy the FRESHEST existing copy (by mtime, across all installed version
|
|
173
|
+
* homes) into `toConfigDir` when its copy is missing or older. mtime is
|
|
174
|
+
* preserved so the "freshest" comparison stays stable and switches don't
|
|
175
|
+
* ping-pong. Best-effort: a failed copy just means the user re-logs in.
|
|
176
|
+
*/
|
|
177
|
+
export declare function carryForwardAuthFiles(agent: AgentId, toConfigDir: string): void;
|
|
162
178
|
export declare function switchConfigSymlink(agent: AgentId, version: string): Promise<{
|
|
163
179
|
success: boolean;
|
|
164
180
|
backupPath?: string;
|