@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
|
@@ -19,6 +19,41 @@ import { localLogPath } from './tasks.js';
|
|
|
19
19
|
function sleep(ms) {
|
|
20
20
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Cap for the LOCAL mirror of a distributed teammate's remote log. `followHostTask`
|
|
24
|
+
* appends remote bytes into the local mirror forever; a team can spin 10+ chatty
|
|
25
|
+
* remote teammates, so the orchestrator must keep a bounded window (the full log
|
|
26
|
+
* always lives on the host). The teams remote path (agents.ts readNewEvents) writes
|
|
27
|
+
* its own append into each teammate's `stdout.log`, then truncates that file to its
|
|
28
|
+
* trailing `REMOTE_MIRROR_MAX_BYTES` — the parser has already consumed the bytes
|
|
29
|
+
* (status/digest updated via lastReadPos), so trailing-tail history is dead weight.
|
|
30
|
+
*/
|
|
31
|
+
export const REMOTE_MIRROR_MAX_BYTES = 512 * 1024;
|
|
32
|
+
/**
|
|
33
|
+
* Pull the new bytes of a remote log since `offset` in ONE ssh round-trip.
|
|
34
|
+
*
|
|
35
|
+
* The teams remote-teammate monitor calls this each poll to advance its offset-tail
|
|
36
|
+
* cursor into the host's log, mirroring only the delta into the local `stdout.log`
|
|
37
|
+
* the stream-json parser consumes. Byte-exact (raw Buffer, no UTF-8 decode) so a
|
|
38
|
+
* multibyte character split at the `tail -c` boundary neither drifts the offset nor
|
|
39
|
+
* renders as U+FFFD — the same discipline `fetchProgress` uses. `bytes.length` is
|
|
40
|
+
* the exact wire count; `newOffset` is `offset + bytes.length`.
|
|
41
|
+
*
|
|
42
|
+
* Returns null on a transient ssh failure (the caller retries next poll without
|
|
43
|
+
* advancing). `remoteLog` is a $HOME-prefixed path with a safe basename; it's
|
|
44
|
+
* shell-quoted defensively even so.
|
|
45
|
+
*/
|
|
46
|
+
export function pullRemoteLogDelta(target, opts) {
|
|
47
|
+
// remoteLog is a dispatch-generated `$HOME/.agents/.cache/hosts/<hex>.log` path —
|
|
48
|
+
// interpolate UNQUOTED so the remote shell expands `$HOME` (shellQuote would
|
|
49
|
+
// single-quote it into a literal `$HOME`, and the tail would find nothing). The
|
|
50
|
+
// hex basename is injection-safe, matching fetchProgress below.
|
|
51
|
+
const remote = `tail -c +${opts.offset + 1} ${opts.remoteLog} 2>/dev/null`;
|
|
52
|
+
const res = sshExecRaw(target, remote, { timeoutMs: 20000, multiplex: true });
|
|
53
|
+
if (res.code === null)
|
|
54
|
+
return null; // ssh itself failed / timed out
|
|
55
|
+
return { bytes: res.stdout, newOffset: opts.offset + res.stdout.length };
|
|
56
|
+
}
|
|
22
57
|
/**
|
|
23
58
|
* Build the per-task sentinel that separates the log tail from the exit-file
|
|
24
59
|
* contents in one combined fetch. The task id (8 hex chars) makes collision with
|
|
@@ -44,7 +44,7 @@ export declare const HOST_ROUTING_SPECS: StripSpec[];
|
|
|
44
44
|
* does not exist). Anything else — including an unknown/absent OS — keeps the
|
|
45
45
|
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
46
46
|
*/
|
|
47
|
-
export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string): string;
|
|
47
|
+
export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string, env?: Record<string, string>): string;
|
|
48
48
|
/** The two remote shell dialects we build commands for. */
|
|
49
49
|
export type RemoteShell = 'posix' | 'powershell';
|
|
50
50
|
/**
|
|
@@ -61,13 +61,23 @@ export const HOST_ROUTING_SPECS = [
|
|
|
61
61
|
* does not exist). Anything else — including an unknown/absent OS — keeps the
|
|
62
62
|
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
63
63
|
*/
|
|
64
|
-
export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os) {
|
|
64
|
+
export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os, env) {
|
|
65
65
|
if (remoteShellFor(os) === 'powershell') {
|
|
66
|
-
return buildWindowsAgentsCommand({ args: forwardedArgs, cwd: remoteCwd });
|
|
66
|
+
return buildWindowsAgentsCommand({ args: forwardedArgs, cwd: remoteCwd, env });
|
|
67
67
|
}
|
|
68
68
|
const inner = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
|
|
69
69
|
const withCwd = remoteCwd ? `cd ${shellQuote(remoteCwd)} && ${inner}` : inner;
|
|
70
|
-
|
|
70
|
+
if (!env || Object.keys(env).length === 0) {
|
|
71
|
+
return `bash -lc ${shellQuote(withCwd)}`;
|
|
72
|
+
}
|
|
73
|
+
// Prepend env exports so the remote command sees the shims dir even when the
|
|
74
|
+
// login shell hasn't sourced the interactive rc files that usually add it.
|
|
75
|
+
// Values are double-quoted (not single-quoted) so remote variables like
|
|
76
|
+
// $HOME and $PATH are expanded by the login shell.
|
|
77
|
+
const exports = Object.entries(env)
|
|
78
|
+
.map(([k, v]) => `export ${shellQuote(k)}="${v.replace(/[\\"]/g, '\\$&')}"`)
|
|
79
|
+
.join('; ');
|
|
80
|
+
return `bash -lc ${shellQuote(`${exports}; ${withCwd}`)}`;
|
|
71
81
|
}
|
|
72
82
|
/**
|
|
73
83
|
* Pick the remote shell dialect from a recorded OS/platform string. A Windows
|
|
@@ -14,7 +14,10 @@ export declare function needsWindowsShell(binary: string, platform?: NodeJS.Plat
|
|
|
14
14
|
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
15
15
|
* `null` if not found. On Windows `where` can return several lines (one per
|
|
16
16
|
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
17
|
-
* shell would actually run, matching `which` semantics on POSIX.
|
|
17
|
+
* shell would actually run, matching `which` semantics on POSIX. stderr is
|
|
18
|
+
* dropped: a miss is an expected outcome here, and `where.exe` announces every
|
|
19
|
+
* miss on stderr ("INFO: Could not find files..."), which would otherwise leak
|
|
20
|
+
* into the caller's terminal once per probe.
|
|
18
21
|
*/
|
|
19
22
|
export declare function findExecutable(name: string, platform?: NodeJS.Platform): string | null;
|
|
20
23
|
/**
|
|
@@ -27,11 +27,17 @@ export function needsWindowsShell(binary, platform = process.platform) {
|
|
|
27
27
|
* Resolve an executable name to its absolute path via the OS PATH search, or
|
|
28
28
|
* `null` if not found. On Windows `where` can return several lines (one per
|
|
29
29
|
* PATHEXT match, e.g. `agents.cmd` and `agents.ps1`) — the first is the one the
|
|
30
|
-
* shell would actually run, matching `which` semantics on POSIX.
|
|
30
|
+
* shell would actually run, matching `which` semantics on POSIX. stderr is
|
|
31
|
+
* dropped: a miss is an expected outcome here, and `where.exe` announces every
|
|
32
|
+
* miss on stderr ("INFO: Could not find files..."), which would otherwise leak
|
|
33
|
+
* into the caller's terminal once per probe.
|
|
31
34
|
*/
|
|
32
35
|
export function findExecutable(name, platform = process.platform) {
|
|
33
36
|
try {
|
|
34
|
-
const out = execFileSync(whichCommand(platform), [name], {
|
|
37
|
+
const out = execFileSync(whichCommand(platform), [name], {
|
|
38
|
+
encoding: 'utf-8',
|
|
39
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
40
|
+
});
|
|
35
41
|
const first = out.trim().split(/\r?\n/)[0]?.trim();
|
|
36
42
|
return first || null;
|
|
37
43
|
}
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -74,11 +74,3 @@ export interface GetAgentResourcesOptions {
|
|
|
74
74
|
* This is the source of truth - not the tracking data in agents.yaml.
|
|
75
75
|
*/
|
|
76
76
|
export declare function getAgentResources(agentId: AgentId, options?: GetAgentResourcesOptions): AgentResources;
|
|
77
|
-
/**
|
|
78
|
-
* Get resources for all agents.
|
|
79
|
-
*/
|
|
80
|
-
export declare function getAllAgentResources(agentIds: AgentId[], options?: GetAgentResourcesOptions & {
|
|
81
|
-
cliStates?: Record<AgentId, {
|
|
82
|
-
installed: boolean;
|
|
83
|
-
}>;
|
|
84
|
-
}): AgentResources[];
|
package/dist/lib/resources.js
CHANGED
|
@@ -180,13 +180,3 @@ export function getAgentResources(agentId, options = {}) {
|
|
|
180
180
|
workflows,
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
|
-
/**
|
|
184
|
-
* Get resources for all agents.
|
|
185
|
-
*/
|
|
186
|
-
export function getAllAgentResources(agentIds, options = {}) {
|
|
187
|
-
const { cliStates, ...restOptions } = options;
|
|
188
|
-
return agentIds.map((agentId) => {
|
|
189
|
-
const cliInstalled = cliStates?.[agentId]?.installed ?? true;
|
|
190
|
-
return getAgentResources(agentId, { ...restOptions, cliInstalled });
|
|
191
|
-
});
|
|
192
|
-
}
|
package/dist/lib/runner.js
CHANGED
|
@@ -74,8 +74,16 @@ export function buildJobCommand(config, resolvedPrompt) {
|
|
|
74
74
|
appendModelAndReasoning(cmd, config);
|
|
75
75
|
}
|
|
76
76
|
if (config.agent === 'codex') {
|
|
77
|
-
if (mode === '
|
|
78
|
-
|
|
77
|
+
if (mode === 'plan') {
|
|
78
|
+
// The template defaults to workspace-write; plan means read-only.
|
|
79
|
+
const sbIndex = cmd.indexOf('--sandbox');
|
|
80
|
+
if (sbIndex !== -1)
|
|
81
|
+
cmd[sbIndex + 1] = 'read-only';
|
|
82
|
+
}
|
|
83
|
+
else if (mode === 'edit' || mode === 'auto') {
|
|
84
|
+
// Keep the workspace-write sandbox — no approval bypass; only skip drops
|
|
85
|
+
// the guardrails. Re-enable network, which workspace-write turns off.
|
|
86
|
+
cmd.push('-c', 'sandbox_workspace_write.network_access=true');
|
|
79
87
|
}
|
|
80
88
|
else if (mode === 'skip') {
|
|
81
89
|
// Remove sandbox restriction, just --dangerously-bypass-approvals-and-sandbox
|
|
Binary file
|
|
Binary file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PidSessionEntry } from './pid-registry.js';
|
|
2
|
-
import { type SessionActivity, type AwaitingReason, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
2
|
+
import { type SessionActivity, type AwaitingReason, type StructuredQuestion, type DetectedPr, type DetectedWorktree, type DetectedTicket } from './state.js';
|
|
3
3
|
import { type SessionProvenance } from './provenance.js';
|
|
4
4
|
/**
|
|
5
5
|
* Per-PID `lsof` probes run bounded and staggered rather than as one parallel
|
|
@@ -30,6 +30,16 @@ export interface ActiveSession {
|
|
|
30
30
|
activity?: SessionActivity;
|
|
31
31
|
/** Why the agent is waiting, when activity is waiting_input. */
|
|
32
32
|
awaitingReason?: AwaitingReason;
|
|
33
|
+
/** The structured decision (question/plan/permission + options) the agent is waiting on. */
|
|
34
|
+
question?: StructuredQuestion;
|
|
35
|
+
/**
|
|
36
|
+
* Plan markdown from the last `ExitPlanMode` tool call. Present when the
|
|
37
|
+
* transcript ever entered plan-review; `awaitingReason === 'plan_review'`
|
|
38
|
+
* says whether it is still pending.
|
|
39
|
+
*/
|
|
40
|
+
plan?: string;
|
|
41
|
+
/** Last few assistant turns (most-recent last), for at-a-glance context in the UI. */
|
|
42
|
+
tail?: string[];
|
|
33
43
|
/** PR opened during the session. */
|
|
34
44
|
pr?: DetectedPr;
|
|
35
45
|
/** Worktree the session runs in. */
|
|
@@ -236,6 +236,9 @@ function applyState(base, state, fallbackFile) {
|
|
|
236
236
|
status: statusFromActivity(state.activity),
|
|
237
237
|
activity: state.activity,
|
|
238
238
|
awaitingReason: state.awaitingReason,
|
|
239
|
+
question: state.question,
|
|
240
|
+
plan: state.plan,
|
|
241
|
+
tail: state.tail,
|
|
239
242
|
// Prefer the live preview (latest turn); keep the first-prompt topic as a fallback.
|
|
240
243
|
preview: state.preview ?? base.preview,
|
|
241
244
|
pr: state.pr,
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export interface SessionRow {
|
|
|
35
35
|
pr_number: number | null;
|
|
36
36
|
worktree_slug: string | null;
|
|
37
37
|
ticket_id: string | null;
|
|
38
|
+
plan: string | null;
|
|
38
39
|
}
|
|
39
40
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
40
41
|
export interface ScanStamp {
|
|
@@ -199,10 +200,6 @@ export interface TopCostSession {
|
|
|
199
200
|
* vanished, mirroring querySessions' liveness filter.
|
|
200
201
|
*/
|
|
201
202
|
export declare function topSessionsByCost(n: number, options?: QueryOptions): TopCostSession[];
|
|
202
|
-
/** Return the set of all file paths currently tracked in the sessions table. */
|
|
203
|
-
export declare function getAllFilePaths(): Set<string>;
|
|
204
|
-
/** Look up sessions by their source file paths. */
|
|
205
|
-
export declare function getSessionsByFilePaths(paths: string[]): Map<string, SessionMeta>;
|
|
206
203
|
/** Look up a single session by its unique ID. */
|
|
207
204
|
export declare function getSessionById(id: string): SessionMeta | null;
|
|
208
205
|
/**
|
package/dist/lib/session/db.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
|
13
13
|
const SESSIONS_DIR = getSessionsDir();
|
|
14
14
|
const DB_PATH = getSessionsDbPath();
|
|
15
15
|
/** Current schema version; bumped when migrations are added. */
|
|
16
|
-
const SCHEMA_VERSION =
|
|
16
|
+
const SCHEMA_VERSION = 11;
|
|
17
17
|
/**
|
|
18
18
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
19
19
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -64,7 +64,8 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
64
64
|
pr_url TEXT,
|
|
65
65
|
pr_number INTEGER,
|
|
66
66
|
worktree_slug TEXT,
|
|
67
|
-
ticket_id TEXT
|
|
67
|
+
ticket_id TEXT,
|
|
68
|
+
plan TEXT
|
|
68
69
|
);
|
|
69
70
|
CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp DESC);
|
|
70
71
|
CREATE INDEX IF NOT EXISTS idx_sessions_cwd ON sessions(cwd);
|
|
@@ -213,6 +214,16 @@ function migrateSchema(db, fromVersion) {
|
|
|
213
214
|
db.exec(`ALTER TABLE sessions DROP COLUMN name`);
|
|
214
215
|
}
|
|
215
216
|
}
|
|
217
|
+
if (fromVersion < 11) {
|
|
218
|
+
// v10 → v11: the Claude scanner now captures the ExitPlanMode plan markdown
|
|
219
|
+
// at scan time so `agents sessions --json` can surface it without forcing
|
|
220
|
+
// consumers (the Factory NEEDS-YOU panel) to re-read raw JSONL. Additive
|
|
221
|
+
// column; rescan to backfill.
|
|
222
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
223
|
+
if (!cols.some(c => c.name === 'plan'))
|
|
224
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN plan TEXT`);
|
|
225
|
+
db.exec(`DELETE FROM scan_ledger;`);
|
|
226
|
+
}
|
|
216
227
|
}
|
|
217
228
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
218
229
|
export function getDB() {
|
|
@@ -429,13 +440,13 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
429
440
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
430
441
|
cost_usd, duration_ms,
|
|
431
442
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
432
|
-
pr_url, pr_number, worktree_slug, ticket_id
|
|
443
|
+
pr_url, pr_number, worktree_slug, ticket_id, plan
|
|
433
444
|
) VALUES (
|
|
434
445
|
@id, @short_id, @agent, @version, @account, @timestamp, @last_activity,
|
|
435
446
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
436
447
|
@cost_usd, @duration_ms,
|
|
437
448
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
438
|
-
@pr_url, @pr_number, @worktree_slug, @ticket_id
|
|
449
|
+
@pr_url, @pr_number, @worktree_slug, @ticket_id, @plan
|
|
439
450
|
)
|
|
440
451
|
ON CONFLICT(id) DO UPDATE SET
|
|
441
452
|
short_id = excluded.short_id,
|
|
@@ -461,7 +472,8 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
461
472
|
pr_url = excluded.pr_url,
|
|
462
473
|
pr_number = excluded.pr_number,
|
|
463
474
|
worktree_slug = excluded.worktree_slug,
|
|
464
|
-
ticket_id = excluded.ticket_id
|
|
475
|
+
ticket_id = excluded.ticket_id,
|
|
476
|
+
plan = excluded.plan
|
|
465
477
|
`);
|
|
466
478
|
const deleteTextStmt = (db) => db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
|
|
467
479
|
const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (session_id, label, topic, project, content) VALUES (?, ?, ?, ?, ?)`);
|
|
@@ -509,6 +521,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
509
521
|
pr_number: meta.prNumber ?? null,
|
|
510
522
|
worktree_slug: meta.worktreeSlug ?? null,
|
|
511
523
|
ticket_id: meta.ticketId ?? null,
|
|
524
|
+
plan: meta.plan ?? null,
|
|
512
525
|
};
|
|
513
526
|
const txn = db.transaction(() => {
|
|
514
527
|
upsert.run(row);
|
|
@@ -597,6 +610,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
597
610
|
pr_number: meta.prNumber ?? null,
|
|
598
611
|
worktree_slug: meta.worktreeSlug ?? null,
|
|
599
612
|
ticket_id: meta.ticketId ?? null,
|
|
613
|
+
plan: meta.plan ?? null,
|
|
600
614
|
});
|
|
601
615
|
delText.run(meta.id);
|
|
602
616
|
insText.run(meta.id, meta.label ?? '', meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
@@ -769,6 +783,7 @@ function rowToMeta(row) {
|
|
|
769
783
|
prNumber: row.pr_number ?? undefined,
|
|
770
784
|
worktreeSlug: row.worktree_slug ?? undefined,
|
|
771
785
|
ticketId: row.ticket_id ?? undefined,
|
|
786
|
+
plan: row.plan ?? undefined,
|
|
772
787
|
};
|
|
773
788
|
}
|
|
774
789
|
/**
|
|
@@ -954,26 +969,6 @@ export function topSessionsByCost(n, options = {}) {
|
|
|
954
969
|
durationMs: r.duration_ms ?? 0,
|
|
955
970
|
}));
|
|
956
971
|
}
|
|
957
|
-
/** Return the set of all file paths currently tracked in the sessions table. */
|
|
958
|
-
export function getAllFilePaths() {
|
|
959
|
-
const db = getDB();
|
|
960
|
-
const rows = db.prepare(`SELECT file_path FROM sessions`).all();
|
|
961
|
-
return new Set(rows.map(r => r.file_path));
|
|
962
|
-
}
|
|
963
|
-
/** Look up sessions by their source file paths. */
|
|
964
|
-
export function getSessionsByFilePaths(paths) {
|
|
965
|
-
if (paths.length === 0)
|
|
966
|
-
return new Map();
|
|
967
|
-
const db = getDB();
|
|
968
|
-
const placeholders = paths.map(() => '?').join(',');
|
|
969
|
-
const rows = db
|
|
970
|
-
.prepare(`SELECT * FROM sessions WHERE file_path IN (${placeholders})`)
|
|
971
|
-
.all(...paths);
|
|
972
|
-
const result = new Map();
|
|
973
|
-
for (const row of rows)
|
|
974
|
-
result.set(row.file_path, rowToMeta(row));
|
|
975
|
-
return result;
|
|
976
|
-
}
|
|
977
972
|
/** Look up a single session by its unique ID. */
|
|
978
973
|
export function getSessionById(id) {
|
|
979
974
|
const db = getDB();
|
|
@@ -68,6 +68,8 @@ interface ClaudeSessionScan {
|
|
|
68
68
|
createdTickets?: string[];
|
|
69
69
|
/** Team name this session SPAWNED via `agents teams create/add` (not team-of-origin). */
|
|
70
70
|
spawnedTeam?: string;
|
|
71
|
+
/** Plan markdown from the last ExitPlanMode tool call (Claude sessions only). */
|
|
72
|
+
plan?: string;
|
|
71
73
|
}
|
|
72
74
|
/**
|
|
73
75
|
* Discover sessions. Scans only files whose (mtime, size) have changed since
|
|
@@ -152,8 +154,6 @@ export declare function readCodexMeta(filePath: string, resolveAccount?: () => s
|
|
|
152
154
|
} | null>;
|
|
153
155
|
/** Stream a Claude JSONL file and extract scan-level metadata (timestamp, cwd, topic, tokens). */
|
|
154
156
|
export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessionScan>;
|
|
155
|
-
/** Read up to maxLines non-empty lines from the beginning of a file. */
|
|
156
|
-
export declare function readFirstLines(filePath: string, maxLines: number): Promise<string[]>;
|
|
157
157
|
/** Parse a single Kimi session state.json file to extract session metadata. */
|
|
158
158
|
export declare function readKimiMeta(filePath: string): {
|
|
159
159
|
meta: SessionMeta;
|
|
@@ -13,6 +13,7 @@ import * as crypto from 'crypto';
|
|
|
13
13
|
import * as readline from 'readline';
|
|
14
14
|
import { execFile } from 'child_process';
|
|
15
15
|
import { promisify } from 'util';
|
|
16
|
+
import Database from '../sqlite.js';
|
|
16
17
|
import { getAgentsDir, getHistoryDir } from '../state.js';
|
|
17
18
|
const execFileAsync = promisify(execFile);
|
|
18
19
|
import { AGENTS, agentConfigDirName, getCliVersion } from '../agents.js';
|
|
@@ -485,6 +486,7 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
|
|
|
485
486
|
ticketId: scan.ticketId,
|
|
486
487
|
createdTickets: scan.createdTickets,
|
|
487
488
|
spawnedTeam: scan.spawnedTeam,
|
|
489
|
+
plan: scan.plan,
|
|
488
490
|
};
|
|
489
491
|
}
|
|
490
492
|
else {
|
|
@@ -510,6 +512,7 @@ async function readClaudeMeta(filePath, sessionId, account, label) {
|
|
|
510
512
|
ticketId: scan.ticketId,
|
|
511
513
|
createdTickets: scan.createdTickets,
|
|
512
514
|
spawnedTeam: scan.spawnedTeam,
|
|
515
|
+
plan: scan.plan,
|
|
513
516
|
};
|
|
514
517
|
}
|
|
515
518
|
return { meta, content: scan.contentText || '' };
|
|
@@ -1065,20 +1068,29 @@ let cachedOpenCodeAccount;
|
|
|
1065
1068
|
async function getOpenCodeAccount() {
|
|
1066
1069
|
if (cachedOpenCodeAccount !== undefined)
|
|
1067
1070
|
return cachedOpenCodeAccount || undefined;
|
|
1071
|
+
// Read through the node/bun SQLite wrapper (not the `sqlite3` CLI) so this
|
|
1072
|
+
// works on every OS — the CLI is absent on Windows.
|
|
1073
|
+
let db;
|
|
1068
1074
|
try {
|
|
1069
1075
|
if (fs.existsSync(OPENCODE_DB)) {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
'SELECT email FROM control_account WHERE active=1 LIMIT 1;'
|
|
1073
|
-
|
|
1074
|
-
const out =
|
|
1076
|
+
db = new Database(OPENCODE_DB);
|
|
1077
|
+
const row = db
|
|
1078
|
+
.prepare('SELECT email FROM control_account WHERE active=1 LIMIT 1;')
|
|
1079
|
+
.get();
|
|
1080
|
+
const out = typeof row?.email === 'string' ? row.email.trim() : '';
|
|
1075
1081
|
if (out) {
|
|
1076
1082
|
cachedOpenCodeAccount = out;
|
|
1077
1083
|
return out;
|
|
1078
1084
|
}
|
|
1079
1085
|
}
|
|
1080
1086
|
}
|
|
1081
|
-
catch { /*
|
|
1087
|
+
catch { /* DB not accessible, sqlite module unavailable, or query failed */ }
|
|
1088
|
+
finally {
|
|
1089
|
+
try {
|
|
1090
|
+
db?.close();
|
|
1091
|
+
}
|
|
1092
|
+
catch { /* best-effort close */ }
|
|
1093
|
+
}
|
|
1082
1094
|
cachedOpenCodeAccount = '';
|
|
1083
1095
|
return undefined;
|
|
1084
1096
|
}
|
|
@@ -1101,18 +1113,21 @@ async function scanOpenCodeIncremental() {
|
|
|
1101
1113
|
}
|
|
1102
1114
|
const account = await getOpenCodeAccount();
|
|
1103
1115
|
const currentVersion = await getCurrentAgentVersion('opencode');
|
|
1116
|
+
// Read through the node/bun SQLite wrapper (not the `sqlite3` CLI) so this
|
|
1117
|
+
// works on every OS — the CLI is absent on Windows.
|
|
1118
|
+
let db;
|
|
1104
1119
|
try {
|
|
1105
1120
|
const query = `
|
|
1106
1121
|
SELECT
|
|
1107
|
-
s.id,
|
|
1108
|
-
s.title,
|
|
1109
|
-
s.directory,
|
|
1110
|
-
s.version,
|
|
1111
|
-
s.time_created,
|
|
1112
|
-
s.time_updated,
|
|
1113
|
-
COALESCE(stats.message_count, 0),
|
|
1114
|
-
stats.token_count,
|
|
1115
|
-
COALESCE(stats.has_token_data, 0)
|
|
1122
|
+
s.id AS id,
|
|
1123
|
+
s.title AS title,
|
|
1124
|
+
s.directory AS directory,
|
|
1125
|
+
s.version AS version,
|
|
1126
|
+
s.time_created AS time_created,
|
|
1127
|
+
s.time_updated AS time_updated,
|
|
1128
|
+
COALESCE(stats.message_count, 0) AS message_count,
|
|
1129
|
+
stats.token_count AS token_count,
|
|
1130
|
+
COALESCE(stats.has_token_data, 0) AS has_token_data
|
|
1116
1131
|
FROM session s
|
|
1117
1132
|
LEFT JOIN (
|
|
1118
1133
|
SELECT
|
|
@@ -1133,23 +1148,22 @@ async function scanOpenCodeIncremental() {
|
|
|
1133
1148
|
ORDER BY time_created DESC
|
|
1134
1149
|
LIMIT 1000;
|
|
1135
1150
|
`.replace(/\n/g, ' ');
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
timeout: 5000,
|
|
1139
|
-
maxBuffer: 32 * 1024 * 1024,
|
|
1140
|
-
});
|
|
1151
|
+
db = new Database(OPENCODE_DB);
|
|
1152
|
+
const rows = db.prepare(query).all();
|
|
1141
1153
|
const entries = [];
|
|
1142
|
-
for (const
|
|
1143
|
-
|
|
1144
|
-
continue;
|
|
1145
|
-
const [id, title, directory, version, timeCreatedStr, timeUpdatedStr, messageCountStr, tokenCountStr, hasTokenDataStr] = line.split('|||');
|
|
1154
|
+
for (const row of rows) {
|
|
1155
|
+
const id = typeof row.id === 'string' ? row.id : '';
|
|
1146
1156
|
if (!id)
|
|
1147
1157
|
continue;
|
|
1148
|
-
const
|
|
1149
|
-
const
|
|
1150
|
-
const
|
|
1151
|
-
const
|
|
1152
|
-
const
|
|
1158
|
+
const title = typeof row.title === 'string' ? row.title : '';
|
|
1159
|
+
const directory = typeof row.directory === 'string' ? row.directory : '';
|
|
1160
|
+
const version = typeof row.version === 'string' ? row.version : '';
|
|
1161
|
+
const asInt = (v) => typeof v === 'number' ? v : parseInt(String(v), 10);
|
|
1162
|
+
const timeCreated = asInt(row.time_created);
|
|
1163
|
+
const timeUpdated = asInt(row.time_updated);
|
|
1164
|
+
const messageCount = asInt(row.message_count);
|
|
1165
|
+
const tokenCount = asInt(row.token_count);
|
|
1166
|
+
const hasTokenData = asInt(row.has_token_data) === 1;
|
|
1153
1167
|
const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
|
|
1154
1168
|
// OpenCode is one shared DB, not one file per session — its row carries a
|
|
1155
1169
|
// per-session updated time. Set lastActivity explicitly (falling back to
|
|
@@ -1182,6 +1196,12 @@ async function scanOpenCodeIncremental() {
|
|
|
1182
1196
|
console.error(`Warning: Could not query OpenCode sessions: ${err.message}`);
|
|
1183
1197
|
}
|
|
1184
1198
|
}
|
|
1199
|
+
finally {
|
|
1200
|
+
try {
|
|
1201
|
+
db?.close();
|
|
1202
|
+
}
|
|
1203
|
+
catch { /* best-effort close */ }
|
|
1204
|
+
}
|
|
1185
1205
|
}
|
|
1186
1206
|
// ---------------------------------------------------------------------------
|
|
1187
1207
|
// OpenClaw
|
|
@@ -1770,6 +1790,9 @@ export async function scanClaudeSession(filePath) {
|
|
|
1770
1790
|
const createdTickets = new Set();
|
|
1771
1791
|
const pendingTicketTools = new Set();
|
|
1772
1792
|
let spawnedTeam;
|
|
1793
|
+
// The LAST ExitPlanMode plan wins so a re-planned session surfaces its most
|
|
1794
|
+
// recent plan, matching the semantic the extension's re-parser relied on.
|
|
1795
|
+
let plan;
|
|
1773
1796
|
try {
|
|
1774
1797
|
for await (const line of rl) {
|
|
1775
1798
|
if (!line.trim())
|
|
@@ -1802,6 +1825,14 @@ export async function scanClaudeSession(filePath) {
|
|
|
1802
1825
|
if (typeof b?.id === 'string' && isTicketCreateTool(b?.name, b?.input?.command)) {
|
|
1803
1826
|
pendingTicketTools.add(b.id);
|
|
1804
1827
|
}
|
|
1828
|
+
// ExitPlanMode plan markdown — last one wins so a re-planned session
|
|
1829
|
+
// reports its most recent plan (the semantic parsePlanFromClaudeJsonl
|
|
1830
|
+
// implemented in the extension).
|
|
1831
|
+
if (b?.name === 'ExitPlanMode' && typeof b?.input?.plan === 'string') {
|
|
1832
|
+
const p = b.input.plan.trim();
|
|
1833
|
+
if (p)
|
|
1834
|
+
plan = b.input.plan;
|
|
1835
|
+
}
|
|
1805
1836
|
}
|
|
1806
1837
|
}
|
|
1807
1838
|
if (pendingTicketTools.size > 0 && parsed.type === 'user' && Array.isArray(parsed.message?.content)) {
|
|
@@ -1949,6 +1980,7 @@ export async function scanClaudeSession(filePath) {
|
|
|
1949
1980
|
ticketId: ticket?.id,
|
|
1950
1981
|
createdTickets: createdTickets.size > 0 ? [...createdTickets] : undefined,
|
|
1951
1982
|
spawnedTeam,
|
|
1983
|
+
plan,
|
|
1952
1984
|
};
|
|
1953
1985
|
}
|
|
1954
1986
|
/** Stream a Codex JSONL file and extract scan-level metadata (session ID, cwd, topic, tokens). */
|
|
@@ -2152,25 +2184,6 @@ function getOpenClawWorkspaceMap() {
|
|
|
2152
2184
|
// ---------------------------------------------------------------------------
|
|
2153
2185
|
// Utilities
|
|
2154
2186
|
// ---------------------------------------------------------------------------
|
|
2155
|
-
/** Read up to maxLines non-empty lines from the beginning of a file. */
|
|
2156
|
-
export function readFirstLines(filePath, maxLines) {
|
|
2157
|
-
return new Promise((resolve) => {
|
|
2158
|
-
const lines = [];
|
|
2159
|
-
const stream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
2160
|
-
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2161
|
-
rl.on('line', (line) => {
|
|
2162
|
-
if (line.trim()) {
|
|
2163
|
-
lines.push(line);
|
|
2164
|
-
}
|
|
2165
|
-
if (lines.length >= maxLines) {
|
|
2166
|
-
rl.close();
|
|
2167
|
-
stream.destroy();
|
|
2168
|
-
}
|
|
2169
|
-
});
|
|
2170
|
-
rl.on('close', () => resolve(lines));
|
|
2171
|
-
rl.on('error', () => resolve(lines));
|
|
2172
|
-
});
|
|
2173
|
-
}
|
|
2174
2187
|
/** Compute the SHA-256 hex digest of a string. */
|
|
2175
2188
|
function sha256(input) {
|
|
2176
2189
|
return crypto.createHash('sha256').update(input).digest('hex');
|