@phnx-labs/agents-cli 1.20.49 → 1.20.51
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/CHANGELOG.md +31 -0
- package/README.md +3 -0
- package/dist/commands/browser-picker.js +1 -18
- package/dist/commands/cloud.js +1 -25
- package/dist/commands/computer.d.ts +1 -0
- package/dist/commands/computer.js +129 -8
- package/dist/commands/doctor.js +133 -2
- package/dist/commands/exec.js +49 -6
- package/dist/commands/factory.js +1 -4
- package/dist/commands/inspect.js +1 -11
- package/dist/commands/mcp.js +2 -6
- package/dist/commands/message.js +1 -4
- package/dist/commands/profiles.js +1 -18
- package/dist/commands/repo.js +33 -14
- package/dist/commands/resource-view.d.ts +1 -0
- package/dist/commands/resource-view.js +5 -17
- package/dist/commands/secrets.d.ts +1 -0
- package/dist/commands/secrets.js +1 -28
- package/dist/commands/sessions-picker.js +1 -18
- package/dist/commands/sessions.js +6 -8
- package/dist/commands/teams-picker.js +1 -32
- package/dist/commands/teams.js +218 -97
- package/dist/commands/tmux.js +1 -3
- package/dist/commands/view.js +1 -9
- package/dist/commands/worktree.js +1 -4
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +20 -33
- package/dist/lib/auto-dispatch-linear.d.ts +18 -0
- package/dist/lib/auto-dispatch-linear.js +107 -0
- package/dist/lib/auto-dispatch-provider.d.ts +10 -0
- package/dist/lib/auto-dispatch-provider.js +25 -0
- package/dist/lib/auto-dispatch.d.ts +87 -0
- package/dist/lib/auto-dispatch.js +142 -0
- package/dist/lib/browser/cdp.js +11 -2
- package/dist/lib/browser/drivers/ssh.d.ts +28 -10
- package/dist/lib/browser/drivers/ssh.js +57 -18
- package/dist/lib/browser/refs.js +1 -5
- package/dist/lib/cli-resources.d.ts +0 -2
- package/dist/lib/cli-resources.js +30 -13
- package/dist/lib/cloud/rush.d.ts +0 -24
- package/dist/lib/cloud/rush.js +0 -31
- package/dist/lib/crabbox/cli.js +4 -1
- package/dist/lib/crabbox/lease.js +29 -1
- package/dist/lib/daemon.js +41 -0
- package/dist/lib/exec.js +43 -18
- package/dist/lib/format.d.ts +38 -0
- package/dist/lib/format.js +108 -0
- package/dist/lib/git.d.ts +21 -0
- package/dist/lib/git.js +92 -0
- package/dist/lib/hooks/cache.d.ts +9 -2
- package/dist/lib/hooks/cache.js +220 -8
- package/dist/lib/hooks.js +17 -8
- package/dist/lib/hosts/passthrough.js +30 -1
- package/dist/lib/hosts/progress.d.ts +31 -0
- package/dist/lib/hosts/progress.js +35 -0
- package/dist/lib/hosts/remote-cmd.d.ts +1 -1
- package/dist/lib/hosts/remote-cmd.js +13 -3
- package/dist/lib/platform/exec.d.ts +4 -1
- package/dist/lib/platform/exec.js +8 -2
- package/dist/lib/resources.d.ts +0 -8
- package/dist/lib/resources.js +0 -10
- package/dist/lib/runner.js +10 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/active.d.ts +11 -1
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/db.d.ts +1 -4
- package/dist/lib/session/db.js +20 -25
- package/dist/lib/session/discover.d.ts +2 -2
- package/dist/lib/session/discover.js +61 -48
- package/dist/lib/session/parse.js +35 -34
- package/dist/lib/session/render.d.ts +7 -3
- package/dist/lib/session/render.js +15 -9
- package/dist/lib/session/state.d.ts +55 -0
- package/dist/lib/session/state.js +87 -10
- package/dist/lib/session/types.d.ts +9 -0
- package/dist/lib/shims.d.ts +25 -2
- package/dist/lib/shims.js +73 -8
- package/dist/lib/ssh-tunnel.d.ts +33 -2
- package/dist/lib/ssh-tunnel.js +94 -7
- package/dist/lib/staleness/types.d.ts +0 -1
- package/dist/lib/teams/agents.d.ts +108 -1
- package/dist/lib/teams/agents.js +511 -11
- package/dist/lib/teams/api.d.ts +7 -1
- package/dist/lib/teams/api.js +5 -2
- package/dist/lib/teams/registry.d.ts +17 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/remoteWorktree.d.ts +57 -0
- package/dist/lib/teams/remoteWorktree.js +213 -0
- package/dist/lib/teams/scheduler.d.ts +29 -0
- package/dist/lib/teams/scheduler.js +78 -0
- package/dist/lib/teams/supervisor.js +7 -0
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/versions.d.ts +7 -26
- package/dist/lib/versions.js +44 -146
- package/dist/lib/warn-unpushed.d.ts +40 -0
- package/dist/lib/warn-unpushed.js +128 -0
- package/package.json +3 -1
- package/dist/lib/resources/index.d.ts +0 -53
- package/dist/lib/resources/index.js +0 -76
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* objects suitable for rendering, filtering, and summarization.
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
|
+
import { truncate } from '../format.js';
|
|
9
10
|
import * as path from 'path';
|
|
10
|
-
import { execFileSync } from 'child_process';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
12
|
/**
|
|
13
13
|
* Largest session file we will load into memory. Above this we throw a clean
|
|
@@ -218,10 +218,6 @@ export function summarizeToolUse(tool, args) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
-
/** Truncate a string to n characters, appending '...' if shortened. */
|
|
222
|
-
function truncate(s, n) {
|
|
223
|
-
return s.length <= n ? s : s.slice(0, n - 3) + '...';
|
|
224
|
-
}
|
|
225
221
|
/** Replace the home directory prefix with ~ for display. */
|
|
226
222
|
function shortenPath(p) {
|
|
227
223
|
const home = process.env.HOME || '';
|
|
@@ -1006,45 +1002,50 @@ export function parseOpenCode(filePath) {
|
|
|
1006
1002
|
if (!dbPath || !sessionId)
|
|
1007
1003
|
return [];
|
|
1008
1004
|
const events = [];
|
|
1005
|
+
// Read through the node/bun SQLite wrapper (not the `sqlite3` CLI) so this
|
|
1006
|
+
// works on every OS — the CLI is absent on Windows.
|
|
1007
|
+
let rows;
|
|
1008
|
+
let db;
|
|
1009
1009
|
try {
|
|
1010
|
-
// Query messages with their parts, ordered chronologically.
|
|
1011
|
-
//
|
|
1010
|
+
// Query messages with their parts, ordered chronologically. Tool parts are
|
|
1011
|
+
// truncated to keep large tool outputs from bloating memory; the session id
|
|
1012
|
+
// is bound as a parameter rather than interpolated.
|
|
1012
1013
|
const query = `
|
|
1013
1014
|
SELECT
|
|
1014
|
-
json_extract(m.data, '$.role'),
|
|
1015
|
-
json_extract(p.data, '$.type'),
|
|
1015
|
+
json_extract(m.data, '$.role') AS role,
|
|
1016
|
+
json_extract(p.data, '$.type') AS part_type,
|
|
1016
1017
|
CASE
|
|
1017
1018
|
WHEN json_extract(p.data, '$.type') = 'tool'
|
|
1018
1019
|
THEN substr(p.data, 1, 2000)
|
|
1019
1020
|
ELSE p.data
|
|
1020
|
-
END,
|
|
1021
|
-
m.time_created
|
|
1021
|
+
END AS part_data,
|
|
1022
|
+
m.time_created AS time_created
|
|
1022
1023
|
FROM message m
|
|
1023
1024
|
JOIN part p ON p.message_id = m.id AND p.session_id = m.session_id
|
|
1024
|
-
WHERE m.session_id =
|
|
1025
|
+
WHERE m.session_id = ?
|
|
1025
1026
|
ORDER BY m.time_created ASC, p.time_created ASC;
|
|
1026
1027
|
`.replace(/\n/g, ' ');
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
const
|
|
1046
|
-
const
|
|
1047
|
-
const timeMs = parseInt(
|
|
1028
|
+
db = new Database(dbPath);
|
|
1029
|
+
rows = db.prepare(query).all(sessionId);
|
|
1030
|
+
}
|
|
1031
|
+
catch {
|
|
1032
|
+
/* DB not accessible, sqlite module unavailable, or query failed */
|
|
1033
|
+
return events;
|
|
1034
|
+
}
|
|
1035
|
+
finally {
|
|
1036
|
+
try {
|
|
1037
|
+
db?.close();
|
|
1038
|
+
}
|
|
1039
|
+
catch {
|
|
1040
|
+
/* best-effort close */
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
for (const row of rows) {
|
|
1045
|
+
const role = typeof row.role === 'string' ? row.role : '';
|
|
1046
|
+
const partType = typeof row.part_type === 'string' ? row.part_type : '';
|
|
1047
|
+
const partDataStr = typeof row.part_data === 'string' ? row.part_data : '';
|
|
1048
|
+
const timeMs = typeof row.time_created === 'number' ? row.time_created : parseInt(String(row.time_created), 10);
|
|
1048
1049
|
const timestamp = isNaN(timeMs) ? new Date().toISOString() : new Date(timeMs).toISOString();
|
|
1049
1050
|
let partData;
|
|
1050
1051
|
try {
|
|
@@ -1112,7 +1113,7 @@ export function parseOpenCode(filePath) {
|
|
|
1112
1113
|
}
|
|
1113
1114
|
}
|
|
1114
1115
|
catch {
|
|
1115
|
-
/*
|
|
1116
|
+
/* malformed row payload — return what we parsed so far */
|
|
1116
1117
|
}
|
|
1117
1118
|
return events;
|
|
1118
1119
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* errors, final message). The markdown renderer emits a full conversation
|
|
7
7
|
* transcript. Filtering by role and turn slicing is handled here as well.
|
|
8
8
|
*/
|
|
9
|
-
import type { SessionEvent } from './types.js';
|
|
9
|
+
import type { SessionEvent, SessionMeta } from './types.js';
|
|
10
10
|
/**
|
|
11
11
|
* Return absPath relative to cwd; fall back to ~/… then absolute.
|
|
12
12
|
*/
|
|
@@ -103,7 +103,11 @@ export interface RenderConversationMarkdownOptions {
|
|
|
103
103
|
}
|
|
104
104
|
export declare function renderConversationMarkdown(events: SessionEvent[], opts?: RenderConversationMarkdownOptions): string;
|
|
105
105
|
/**
|
|
106
|
-
* Render session
|
|
106
|
+
* Render one session's JSON output — the shape `agents sessions <id> --json`
|
|
107
|
+
* emits. When `meta` is provided (the standard path), returns a
|
|
108
|
+
* `{ session, events }` wrapper so top-level fields (`plan`, `prUrl`, …) that
|
|
109
|
+
* live on `SessionMeta` are visible without re-parsing events. Legacy
|
|
110
|
+
* callers that pass no meta still get a bare `SessionEvent[]` array.
|
|
107
111
|
*/
|
|
108
|
-
export declare function renderJson(events: SessionEvent[]): string;
|
|
112
|
+
export declare function renderJson(events: SessionEvent[], meta?: SessionMeta): string;
|
|
109
113
|
export {};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* transcript. Filtering by role and turn slicing is handled here as well.
|
|
8
8
|
*/
|
|
9
9
|
import chalk from 'chalk';
|
|
10
|
+
import { truncate } from '../format.js';
|
|
10
11
|
import { summarizeToolUse } from './parse.js';
|
|
11
12
|
import { cleanSessionPrompt, extractSessionTopic } from './prompt.js';
|
|
12
13
|
import { renderMarkdown } from '../markdown.js';
|
|
@@ -336,14 +337,14 @@ function renderCommandsSection(cmds, lines) {
|
|
|
336
337
|
if (catEntry.signal === 'mid') {
|
|
337
338
|
// Mid signal: display the bucket key (e.g. ssh→openclaw browser) with aggregate count
|
|
338
339
|
const countSuffix = v.count > 1 ? chalk.gray(` × ${v.count}`) : '';
|
|
339
|
-
lines.push(` ${chalk.cyan(
|
|
340
|
+
lines.push(` ${chalk.cyan(truncate(key, 80))}${countSuffix}`);
|
|
340
341
|
}
|
|
341
342
|
else {
|
|
342
343
|
// High signal: display distinct raw sample commands
|
|
343
344
|
const distinctSamples = pickDistinct(v.samples, 3);
|
|
344
345
|
for (const sample of distinctSamples) {
|
|
345
346
|
const countSuffix = v.count > 1 ? chalk.gray(` × ${v.count}`) : '';
|
|
346
|
-
lines.push(` ${chalk.cyan(
|
|
347
|
+
lines.push(` ${chalk.cyan(truncate(sample, 80))}${countSuffix}`);
|
|
347
348
|
}
|
|
348
349
|
}
|
|
349
350
|
shown++;
|
|
@@ -354,7 +355,7 @@ function renderCommandsSection(cmds, lines) {
|
|
|
354
355
|
lines.push(` ${chalk.dim('Other')} ${chalk.gray(`(${otherCount})`)}`);
|
|
355
356
|
for (const [, v] of Array.from(otherKeys.entries()).slice(0, 5)) {
|
|
356
357
|
const countSuffix = v.count > 1 ? chalk.gray(` × ${v.count}`) : '';
|
|
357
|
-
lines.push(` ${chalk.cyan(
|
|
358
|
+
lines.push(` ${chalk.cyan(truncate(v.samples[0] ?? '', 80))}${countSuffix}`);
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
lines.push('');
|
|
@@ -372,9 +373,6 @@ function pickDistinct(samples, max) {
|
|
|
372
373
|
}
|
|
373
374
|
return result.length > 0 ? result : samples.slice(0, max);
|
|
374
375
|
}
|
|
375
|
-
function truncateCmd(cmd, max) {
|
|
376
|
-
return cmd.length <= max ? cmd : cmd.slice(0, max - 1) + '…';
|
|
377
|
-
}
|
|
378
376
|
// ── File grouping ─────────────────────────────────────────────────────────────
|
|
379
377
|
/** Group file paths by their parent directory, relative to cwd. */
|
|
380
378
|
function groupByParentDir(paths, cwd) {
|
|
@@ -916,10 +914,18 @@ export function renderConversationMarkdown(events, opts = {}) {
|
|
|
916
914
|
return parts.join('\n\n');
|
|
917
915
|
}
|
|
918
916
|
/**
|
|
919
|
-
* Render session
|
|
917
|
+
* Render one session's JSON output — the shape `agents sessions <id> --json`
|
|
918
|
+
* emits. When `meta` is provided (the standard path), returns a
|
|
919
|
+
* `{ session, events }` wrapper so top-level fields (`plan`, `prUrl`, …) that
|
|
920
|
+
* live on `SessionMeta` are visible without re-parsing events. Legacy
|
|
921
|
+
* callers that pass no meta still get a bare `SessionEvent[]` array.
|
|
920
922
|
*/
|
|
921
|
-
export function renderJson(events) {
|
|
922
|
-
|
|
923
|
+
export function renderJson(events, meta) {
|
|
924
|
+
if (!meta)
|
|
925
|
+
return JSON.stringify(events, null, 2);
|
|
926
|
+
// Strip internal-only bookkeeping fields the listing --json path also strips.
|
|
927
|
+
const { _matchedTerms, _bm25Score, _remote, ...session } = meta;
|
|
928
|
+
return JSON.stringify({ session, events }, null, 2);
|
|
923
929
|
}
|
|
924
930
|
/** Replace the home directory prefix with ~ for trace display. */
|
|
925
931
|
function shortenPathTrace(p) {
|
|
@@ -17,6 +17,30 @@
|
|
|
17
17
|
import type { SessionEvent } from './types.js';
|
|
18
18
|
export type SessionActivity = 'working' | 'waiting_input' | 'idle';
|
|
19
19
|
export type AwaitingReason = 'question' | 'plan_review' | 'permission';
|
|
20
|
+
/** One discrete choice the agent offered the user. */
|
|
21
|
+
export interface QuestionOption {
|
|
22
|
+
/** The choice label — also what a free-text reply channel sends back. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Optional longer description shown under the label. */
|
|
25
|
+
description?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Selection keystroke for an interactive TUI prompt (AskUserQuestion / plan /
|
|
28
|
+
* permission are select-lists, not text inputs): a digit ('1'), or 'esc' to
|
|
29
|
+
* cancel/deny. Absent for a plain prose question, which takes free text.
|
|
30
|
+
*/
|
|
31
|
+
key?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The decision an agent handed back to the user, extracted at the SOURCE so every
|
|
35
|
+
* consumer (Factory panel, teams, cloud) gets the real question + options instead
|
|
36
|
+
* of re-deriving them from a truncated preview line. `reason` mirrors
|
|
37
|
+
* {@link AwaitingReason}; `options` is present when the agent offered discrete choices.
|
|
38
|
+
*/
|
|
39
|
+
export interface StructuredQuestion {
|
|
40
|
+
text: string;
|
|
41
|
+
reason: AwaitingReason;
|
|
42
|
+
options?: QuestionOption[];
|
|
43
|
+
}
|
|
20
44
|
export interface DetectedPr {
|
|
21
45
|
url: string;
|
|
22
46
|
number?: number;
|
|
@@ -40,6 +64,21 @@ export interface SessionState {
|
|
|
40
64
|
lastEventKind?: SessionEvent['type'];
|
|
41
65
|
/** Single-line description of the latest turn (message text or tool action). */
|
|
42
66
|
preview?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The structured decision the agent is waiting on (question / plan / permission),
|
|
69
|
+
* with its options when it offered any. Set only when activity is waiting_input.
|
|
70
|
+
*/
|
|
71
|
+
question?: StructuredQuestion;
|
|
72
|
+
/**
|
|
73
|
+
* The plan markdown from the most recent `ExitPlanMode` tool call, surfaced
|
|
74
|
+
* when `awaitingReason === 'plan_review'`. The state engine detects the
|
|
75
|
+
* handoff off the same tool event; carrying the plan text alongside it lets
|
|
76
|
+
* consumers (the Factory NEEDS-YOU panel, `agents sessions <id> --json`)
|
|
77
|
+
* render the actual plan without re-parsing the transcript.
|
|
78
|
+
*/
|
|
79
|
+
plan?: string;
|
|
80
|
+
/** Last few assistant turns (most-recent last), one line each — panel context. */
|
|
81
|
+
tail?: string[];
|
|
43
82
|
lastActivityMs?: number;
|
|
44
83
|
pr?: DetectedPr;
|
|
45
84
|
worktree?: DetectedWorktree;
|
|
@@ -85,6 +124,22 @@ export declare function isTicketCreateTool(name?: string, command?: string): boo
|
|
|
85
124
|
* take `#<number>`. Returns undefined when neither shape is present.
|
|
86
125
|
*/
|
|
87
126
|
export declare function extractCreatedTicket(text?: string): string | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* Pull the plan markdown out of an `ExitPlanMode` tool_use event's args. The
|
|
129
|
+
* Claude tool schema is `{ plan: string }`; the transcript parser already
|
|
130
|
+
* lifts `input` onto `event.args`. Returns undefined for a missing/empty plan
|
|
131
|
+
* so consumers can rely on `plan?: string` truthiness.
|
|
132
|
+
*/
|
|
133
|
+
export declare function extractPlanText(args?: Record<string, any>): string | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* Structured question from a Claude `AskUserQuestion` tool call. Its input is
|
|
136
|
+
* `{ questions: [{ question, header, options: [{label, description}] }] }` and the
|
|
137
|
+
* whole thing is already parsed onto `event.args` by the transcript parser — this
|
|
138
|
+
* surfaces the first question + its options (instead of collapsing to a generic
|
|
139
|
+
* "Asked you a question"). The prompt is a select-list, so each option carries its
|
|
140
|
+
* 1-based selection digit as `key`.
|
|
141
|
+
*/
|
|
142
|
+
export declare function structuredQuestionFromAsk(args?: Record<string, any>): StructuredQuestion | undefined;
|
|
88
143
|
/**
|
|
89
144
|
* Infer live activity + a preview from a chronological event slice. `pr` /
|
|
90
145
|
* `ticket` / `worktree` are attached by `inferSessionState`; this focuses on the
|
|
@@ -130,6 +130,69 @@ export function extractCreatedTicket(text) {
|
|
|
130
130
|
return `#${gh[1]}`;
|
|
131
131
|
return undefined;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Pull the plan markdown out of an `ExitPlanMode` tool_use event's args. The
|
|
135
|
+
* Claude tool schema is `{ plan: string }`; the transcript parser already
|
|
136
|
+
* lifts `input` onto `event.args`. Returns undefined for a missing/empty plan
|
|
137
|
+
* so consumers can rely on `plan?: string` truthiness.
|
|
138
|
+
*/
|
|
139
|
+
export function extractPlanText(args) {
|
|
140
|
+
const plan = args?.plan;
|
|
141
|
+
if (typeof plan !== 'string')
|
|
142
|
+
return undefined;
|
|
143
|
+
const trimmed = plan.trim();
|
|
144
|
+
return trimmed ? plan : undefined;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Structured question from a Claude `AskUserQuestion` tool call. Its input is
|
|
148
|
+
* `{ questions: [{ question, header, options: [{label, description}] }] }` and the
|
|
149
|
+
* whole thing is already parsed onto `event.args` by the transcript parser — this
|
|
150
|
+
* surfaces the first question + its options (instead of collapsing to a generic
|
|
151
|
+
* "Asked you a question"). The prompt is a select-list, so each option carries its
|
|
152
|
+
* 1-based selection digit as `key`.
|
|
153
|
+
*/
|
|
154
|
+
export function structuredQuestionFromAsk(args) {
|
|
155
|
+
const q = Array.isArray(args?.questions) ? args.questions[0] : undefined;
|
|
156
|
+
if (!q)
|
|
157
|
+
return undefined;
|
|
158
|
+
const text = oneLine(String(q.question ?? q.header ?? '')) || 'Asked you a question';
|
|
159
|
+
const raw = Array.isArray(q.options) ? q.options : [];
|
|
160
|
+
const options = [];
|
|
161
|
+
for (const o of raw) {
|
|
162
|
+
const label = typeof o === 'string' ? oneLine(o) : o?.label != null ? oneLine(String(o.label)) : '';
|
|
163
|
+
if (!label)
|
|
164
|
+
continue;
|
|
165
|
+
const description = typeof o === 'object' && o?.description != null ? oneLine(String(o.description)) : undefined;
|
|
166
|
+
options.push({ label, description, key: String(options.length + 1) });
|
|
167
|
+
}
|
|
168
|
+
return { text, reason: 'question', options: options.length ? options : undefined };
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Canonical approve/deny choices for an interactive prompt that carries no
|
|
172
|
+
* agent-supplied option list: Claude's plan-review and permission dialogs. Approve
|
|
173
|
+
* is reliably option 1; deny/keep-planning maps to ESC, which cancels the prompt in
|
|
174
|
+
* every variant (2- or 3-option) — safer than guessing a digit that could differ.
|
|
175
|
+
*/
|
|
176
|
+
function planReviewQuestion() {
|
|
177
|
+
return {
|
|
178
|
+
text: 'Plan ready — review it',
|
|
179
|
+
reason: 'plan_review',
|
|
180
|
+
options: [
|
|
181
|
+
{ label: 'Approve plan', key: '1' },
|
|
182
|
+
{ label: 'Keep planning', key: 'esc' },
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function permissionQuestion(preview) {
|
|
187
|
+
return {
|
|
188
|
+
text: preview ? `Permission — ${preview}` : 'Waiting on a permission prompt',
|
|
189
|
+
reason: 'permission',
|
|
190
|
+
options: [
|
|
191
|
+
{ label: 'Approve', key: '1' },
|
|
192
|
+
{ label: 'Deny', key: 'esc' },
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
133
196
|
/** Does an assistant message read as a question directed at the user? */
|
|
134
197
|
function looksLikeQuestion(text) {
|
|
135
198
|
const t = text.trim();
|
|
@@ -177,18 +240,27 @@ export function inferActivity(events, ctx = {}) {
|
|
|
177
240
|
const lastToolUse = lastOf(meaningful, e => e.type === 'tool_use');
|
|
178
241
|
// The most informative recent line: a message or tool call as-is, but for a
|
|
179
242
|
// trailing tool_result/thinking show the tool *call* that produced it (its
|
|
180
|
-
// command) rather than a bare
|
|
243
|
+
// command), or — failing that — the last assistant message, rather than a bare
|
|
244
|
+
// "↳ Bash" or a content-free "thinking…". This is what stops a trailing thinking
|
|
245
|
+
// block from masking the real turn (the defect behind the "Thinking…" panel).
|
|
181
246
|
const previewSource = !last
|
|
182
247
|
? undefined
|
|
183
248
|
: last.type === 'message' || last.type === 'tool_use'
|
|
184
249
|
? last
|
|
185
|
-
: (lastToolUse ?? last);
|
|
250
|
+
: (lastToolUse ?? lastMsg ?? last);
|
|
251
|
+
// Last few assistant turns, most-recent last — context for the decision panel.
|
|
252
|
+
const tail = meaningful
|
|
253
|
+
.filter(e => e.type === 'message' && e.role === 'assistant' && e.content)
|
|
254
|
+
.slice(-3)
|
|
255
|
+
.map(e => oneLine(e.content ?? ''))
|
|
256
|
+
.filter(Boolean);
|
|
186
257
|
const base = {
|
|
187
258
|
activity: 'idle',
|
|
188
259
|
lastRole: lastMsg?.role,
|
|
189
260
|
lastEventKind: last?.type,
|
|
190
261
|
lastActivityMs: ctx.mtimeMs,
|
|
191
262
|
preview: previewSource ? describeEvent(previewSource) : undefined,
|
|
263
|
+
tail: tail.length ? tail : undefined,
|
|
192
264
|
};
|
|
193
265
|
if (!last)
|
|
194
266
|
return base;
|
|
@@ -196,12 +268,15 @@ export function inferActivity(events, ctx = {}) {
|
|
|
196
268
|
// tool and nothing has come after it.
|
|
197
269
|
const lastPlanOrAsk = lastOf(meaningful, e => e.type === 'tool_use' && (e.tool === PLAN_TOOL || e.tool === ASK_TOOL));
|
|
198
270
|
if (lastPlanOrAsk && meaningful.indexOf(lastPlanOrAsk) === meaningful.length - 1) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
271
|
+
if (lastPlanOrAsk.tool === PLAN_TOOL) {
|
|
272
|
+
const question = planReviewQuestion();
|
|
273
|
+
const plan = extractPlanText(lastPlanOrAsk.args);
|
|
274
|
+
return { ...base, activity: 'waiting_input', awaitingReason: 'plan_review', preview: question.text, question, plan };
|
|
275
|
+
}
|
|
276
|
+
// AskUserQuestion: surface the real question + options (they're on `args`),
|
|
277
|
+
// not the generic "Asked you a question" that discarded them.
|
|
278
|
+
const question = structuredQuestionFromAsk(lastPlanOrAsk.args) ?? { text: 'Asked you a question', reason: 'question' };
|
|
279
|
+
return { ...base, activity: 'waiting_input', awaitingReason: 'question', preview: question.text, question };
|
|
205
280
|
}
|
|
206
281
|
// Pending tool call (tool_use with no following tool_result): mid-turn.
|
|
207
282
|
if (last.type === 'tool_use') {
|
|
@@ -209,7 +284,7 @@ export function inferActivity(events, ctx = {}) {
|
|
|
209
284
|
return { ...base, activity: 'working' };
|
|
210
285
|
// Alive but the file hasn't moved — likely blocked on a permission prompt.
|
|
211
286
|
if (ctx.pidAlive)
|
|
212
|
-
return { ...base, activity: 'waiting_input', awaitingReason: 'permission' };
|
|
287
|
+
return { ...base, activity: 'waiting_input', awaitingReason: 'permission', question: permissionQuestion(base.preview) };
|
|
213
288
|
return { ...base, activity: 'idle' };
|
|
214
289
|
}
|
|
215
290
|
// Thinking or a tool result just landed → agent is mid-turn if recently active.
|
|
@@ -223,8 +298,10 @@ export function inferActivity(events, ctx = {}) {
|
|
|
223
298
|
return { ...base, activity: canWork ? 'working' : 'idle' };
|
|
224
299
|
}
|
|
225
300
|
// Assistant spoke last and stopped. A trailing question → waiting; else idle.
|
|
301
|
+
// A prose question takes a free-text reply (no select-list), so no options/keys.
|
|
226
302
|
if (looksLikeQuestion(last.content ?? '')) {
|
|
227
|
-
|
|
303
|
+
const text = oneLine(last.content ?? '');
|
|
304
|
+
return { ...base, activity: 'waiting_input', awaitingReason: 'question', question: { text, reason: 'question' } };
|
|
228
305
|
}
|
|
229
306
|
return { ...base, activity: 'idle' };
|
|
230
307
|
}
|
|
@@ -97,6 +97,15 @@ export interface SessionMeta {
|
|
|
97
97
|
* `isTeamOrigin` (which marks sessions spawned BY a team).
|
|
98
98
|
*/
|
|
99
99
|
spawnedTeam?: string;
|
|
100
|
+
/**
|
|
101
|
+
* The plan markdown from the LAST `ExitPlanMode` tool call in the transcript
|
|
102
|
+
* (Claude sessions only), captured at scan time. Present whenever the session
|
|
103
|
+
* ever entered plan-review; consumers can pair it with a live
|
|
104
|
+
* `awaitingReason === 'plan_review'` to decide whether it is still pending.
|
|
105
|
+
* Fills the gap that forced the Factory extension to re-read raw JSONL to
|
|
106
|
+
* recover the plan text — the CLI now carries it on the metadata row.
|
|
107
|
+
*/
|
|
108
|
+
plan?: string;
|
|
100
109
|
/**
|
|
101
110
|
* True when the session was spawned programmatically (SDK entrypoint) rather
|
|
102
111
|
* than by a human at the Claude CLI. Captured at scan time from the JSONL
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export interface ConflictInfo {
|
|
|
77
77
|
* top-level entry add/remove — deep edits to plugin contents won't
|
|
78
78
|
* trigger auto-resync, run `agents sync` for that.
|
|
79
79
|
*/
|
|
80
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
80
|
+
export declare const SHIM_SCHEMA_VERSION = 26;
|
|
81
81
|
/**
|
|
82
82
|
* Generate the full bash shim script for the given agent. The returned string
|
|
83
83
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
|
@@ -145,8 +145,14 @@ export declare function removeShim(agent: AgentId): boolean;
|
|
|
145
145
|
* editor opens the script and the agent never launches. The `.cmd` is
|
|
146
146
|
* now the only Windows artifact and carries this alias marker so
|
|
147
147
|
* staleness checks read it directly.
|
|
148
|
+
* v13 — grok aliases resolve the binary from the versioned home's
|
|
149
|
+
* .grok/downloads first, then fall back to the global ~/.grok/downloads.
|
|
150
|
+
* The old template checked only the global dir, so a `grok@<version>`
|
|
151
|
+
* alias failed with "not installed" whenever the binary was staged into
|
|
152
|
+
* the versioned home (installer run with GROK_HOME set, or grok
|
|
153
|
+
* self-update under the shim).
|
|
148
154
|
*/
|
|
149
|
-
export declare const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
155
|
+
export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 13;
|
|
150
156
|
/**
|
|
151
157
|
* Generate a versioned alias script that directly execs a specific version.
|
|
152
158
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -203,6 +209,23 @@ export declare function removeVersionedAlias(agent: AgentId, version: string): b
|
|
|
203
209
|
* Check if a versioned alias exists (the on-disk artifact for this platform).
|
|
204
210
|
*/
|
|
205
211
|
export declare function versionedAliasExists(agent: AgentId, version: string): boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Read the user's configured Codex model from their active `~/.codex/config.toml`.
|
|
214
|
+
*
|
|
215
|
+
* Codex runs under a per-version `CODEX_HOME` (see `buildExecEnv`). A dispatch
|
|
216
|
+
* pinned to a version whose home config lacks a top-level `model` key silently
|
|
217
|
+
* falls back to Codex's built-in default (currently `gpt-5.3-codex`), which a
|
|
218
|
+
* ChatGPT-tier account is not entitled to use — the run dies with HTTP 400
|
|
219
|
+
* before doing any work. The user's model preference lives in whichever
|
|
220
|
+
* version-home was active when they set it (`~/.codex` symlinks to it), so it is
|
|
221
|
+
* NOT visible to a run pinned to a different version. Forwarding it via `--model`
|
|
222
|
+
* keeps the user's "default model setup" regardless of the active version-home,
|
|
223
|
+
* and is concurrency-safe (no file writes) when fanning out many parallel runs.
|
|
224
|
+
*
|
|
225
|
+
* Returns the top-level `model` only (ignores `[profile.*]` tables). Best-effort:
|
|
226
|
+
* a missing/unreadable/unset config yields `undefined` (caller keeps prior behaviour).
|
|
227
|
+
*/
|
|
228
|
+
export declare function readCodexConfiguredModel(): string | undefined;
|
|
206
229
|
/**
|
|
207
230
|
* Switch the agent's config symlink to point to a specific version.
|
|
208
231
|
* e.g., ~/.claude -> ~/.agents/versions/claude/2.0.65/home/.claude/
|
package/dist/lib/shims.js
CHANGED
|
@@ -214,7 +214,14 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
214
214
|
// v25 — dispatcher self-recovery: if the baked AGENTS_BIN is gone (a removed/moved
|
|
215
215
|
// dev build that generated the shim), resolve `agents` on PATH instead of
|
|
216
216
|
// exiting 127, so a stale/vanished dev build can't brick every launch.
|
|
217
|
-
|
|
217
|
+
// v26 — grok resolves its binary from the versioned home's .grok/downloads first
|
|
218
|
+
// ($VERSION_DIR/home/.grok/downloads, where the binary lands when the
|
|
219
|
+
// installer runs with GROK_HOME set or grok self-updates under the shim),
|
|
220
|
+
// then falls back to the global ~/.grok/downloads for pre-fix installs.
|
|
221
|
+
// The old dispatcher checked only the global dir, so a pinned grok that
|
|
222
|
+
// installed into the versioned home fell through to the "not installed"
|
|
223
|
+
// error.
|
|
224
|
+
export const SHIM_SCHEMA_VERSION = 26;
|
|
218
225
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
219
226
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
220
227
|
function shellQuote(value) {
|
|
@@ -468,11 +475,14 @@ fi
|
|
|
468
475
|
|
|
469
476
|
VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
|
|
470
477
|
|
|
471
|
-
# Grok special case: binary lives in
|
|
472
|
-
#
|
|
478
|
+
# Grok special case: binary lives in the versioned home's .grok/downloads (or,
|
|
479
|
+
# for pre-fix installs, the global ~/.grok/downloads), not node_modules. We
|
|
480
|
+
# still use the agents-cli version dir purely for GROK_HOME isolation.
|
|
473
481
|
if [ "$AGENT" = "grok" ]; then
|
|
474
|
-
#
|
|
475
|
-
|
|
482
|
+
# Check the versioned home first — this is where the binary lands when the
|
|
483
|
+
# installer runs with GROK_HOME set (i.e. via the shim or a correct
|
|
484
|
+
# \`agents add grok\`), or when grok self-updates from within the shim.
|
|
485
|
+
GROK_DOWNLOADS="$VERSION_DIR/home/.grok/downloads"
|
|
476
486
|
if [ -d "$GROK_DOWNLOADS" ]; then
|
|
477
487
|
# Prefer a binary whose filename contains the exact version
|
|
478
488
|
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
|
|
@@ -481,6 +491,17 @@ if [ "$AGENT" = "grok" ]; then
|
|
|
481
491
|
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
|
|
482
492
|
fi
|
|
483
493
|
fi
|
|
494
|
+
# Fall back to the global grok home (binary installed without GROK_HOME set,
|
|
495
|
+
# e.g. an earlier \`agents add grok@latest\` before this resolution fix).
|
|
496
|
+
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
497
|
+
GROK_DOWNLOADS="$HOME/.grok/downloads"
|
|
498
|
+
if [ -d "$GROK_DOWNLOADS" ]; then
|
|
499
|
+
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
|
|
500
|
+
if [ -z "$BINARY" ]; then
|
|
501
|
+
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
|
|
502
|
+
fi
|
|
503
|
+
fi
|
|
504
|
+
fi
|
|
484
505
|
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
485
506
|
# Last resort: the adopted native launcher (recorded absolute path) if we
|
|
486
507
|
# adopted grok, else whatever is on PATH. Prefer the adopted record — after
|
|
@@ -744,8 +765,14 @@ export function removeShim(agent) {
|
|
|
744
765
|
* editor opens the script and the agent never launches. The `.cmd` is
|
|
745
766
|
* now the only Windows artifact and carries this alias marker so
|
|
746
767
|
* staleness checks read it directly.
|
|
768
|
+
* v13 — grok aliases resolve the binary from the versioned home's
|
|
769
|
+
* .grok/downloads first, then fall back to the global ~/.grok/downloads.
|
|
770
|
+
* The old template checked only the global dir, so a `grok@<version>`
|
|
771
|
+
* alias failed with "not installed" whenever the binary was staged into
|
|
772
|
+
* the versioned home (installer run with GROK_HOME set, or grok
|
|
773
|
+
* self-update under the shim).
|
|
747
774
|
*/
|
|
748
|
-
export const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
775
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 13;
|
|
749
776
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
750
777
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
751
778
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -822,13 +849,22 @@ export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/
|
|
|
822
849
|
// "agents __shim" which resolves via getBinaryPath() instead.
|
|
823
850
|
const versionDir = `$HOME/.agents/.history/versions/${agent}/${version}`;
|
|
824
851
|
const binaryResolution = agent === 'grok'
|
|
825
|
-
? `# Grok ships its native binary in
|
|
826
|
-
|
|
852
|
+
? `# Grok ships its native binary in the versioned home's .grok/downloads (or,
|
|
853
|
+
# for pre-fix installs, the global ~/.grok/downloads), not node_modules.
|
|
854
|
+
GROK_DOWNLOADS="${versionDir}/home/.grok/downloads"
|
|
827
855
|
BINARY=""
|
|
828
856
|
if [ -d "$GROK_DOWNLOADS" ]; then
|
|
829
857
|
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
|
|
830
858
|
[ -n "$BINARY" ] || BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
|
|
831
859
|
fi
|
|
860
|
+
# Fall back to the global grok home (binary installed without GROK_HOME set).
|
|
861
|
+
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
862
|
+
GROK_GLOBAL_DOWNLOADS="$HOME/.grok/downloads"
|
|
863
|
+
if [ -d "$GROK_GLOBAL_DOWNLOADS" ]; then
|
|
864
|
+
BINARY=$(ls "$GROK_GLOBAL_DOWNLOADS"/grok-* 2>/dev/null | grep -i "${version}" | head -1)
|
|
865
|
+
[ -n "$BINARY" ] || BINARY=$(ls "$GROK_GLOBAL_DOWNLOADS"/grok-* 2>/dev/null | head -1)
|
|
866
|
+
fi
|
|
867
|
+
fi
|
|
832
868
|
# Refuse a PATH match under our own shims dir — it resolves to this alias's
|
|
833
869
|
# sibling dispatcher shim (shims dir is ahead of ~/.local/bin on PATH) and
|
|
834
870
|
# re-execs forever. Fall through to the clean "not installed" error instead.
|
|
@@ -1008,6 +1044,35 @@ function getAgentConfigPath(agent) {
|
|
|
1008
1044
|
const home = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1009
1045
|
return agentConfig.configDir.replace(os.homedir(), home);
|
|
1010
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Read the user's configured Codex model from their active `~/.codex/config.toml`.
|
|
1049
|
+
*
|
|
1050
|
+
* Codex runs under a per-version `CODEX_HOME` (see `buildExecEnv`). A dispatch
|
|
1051
|
+
* pinned to a version whose home config lacks a top-level `model` key silently
|
|
1052
|
+
* falls back to Codex's built-in default (currently `gpt-5.3-codex`), which a
|
|
1053
|
+
* ChatGPT-tier account is not entitled to use — the run dies with HTTP 400
|
|
1054
|
+
* before doing any work. The user's model preference lives in whichever
|
|
1055
|
+
* version-home was active when they set it (`~/.codex` symlinks to it), so it is
|
|
1056
|
+
* NOT visible to a run pinned to a different version. Forwarding it via `--model`
|
|
1057
|
+
* keeps the user's "default model setup" regardless of the active version-home,
|
|
1058
|
+
* and is concurrency-safe (no file writes) when fanning out many parallel runs.
|
|
1059
|
+
*
|
|
1060
|
+
* Returns the top-level `model` only (ignores `[profile.*]` tables). Best-effort:
|
|
1061
|
+
* a missing/unreadable/unset config yields `undefined` (caller keeps prior behaviour).
|
|
1062
|
+
*/
|
|
1063
|
+
export function readCodexConfiguredModel() {
|
|
1064
|
+
try {
|
|
1065
|
+
const cfg = path.join(getAgentConfigPath('codex'), 'config.toml');
|
|
1066
|
+
const text = fs.readFileSync(cfg, 'utf-8');
|
|
1067
|
+
// Only trust keys before the first [table]; a `model` under [profile.x] is
|
|
1068
|
+
// not the default the CLI uses at top level.
|
|
1069
|
+
const topLevel = text.split(/^\s*\[/m)[0];
|
|
1070
|
+
return topLevel.match(/^\s*model\s*=\s*["']([^"']+)["']/m)?.[1];
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
return undefined;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1011
1076
|
/**
|
|
1012
1077
|
* Get the path to the version's config directory.
|
|
1013
1078
|
* e.g., ~/.agents/versions/claude/2.0.65/home/.claude/
|