@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub webhook trigger receiver for routines.
|
|
3
|
+
*
|
|
4
|
+
* A routine may declare a `trigger` block instead of (or alongside) a cron
|
|
5
|
+
* `schedule` (see `JobConfig.trigger` in `../routines.ts`). This module turns
|
|
6
|
+
* an incoming GitHub webhook into the set of routines it should fire, and
|
|
7
|
+
* dispatches those routines through the exact same path a cron fire uses
|
|
8
|
+
* (`executeJobDetached`).
|
|
9
|
+
*
|
|
10
|
+
* The matching logic (`matchJobsToWebhook`) is a pure, side-effect-free
|
|
11
|
+
* function so it can be unit-tested without a running daemon or http server.
|
|
12
|
+
* The optional http listener (`startWebhookServer`) is a thin adapter over it.
|
|
13
|
+
*/
|
|
14
|
+
import * as http from 'http';
|
|
15
|
+
import type { JobConfig, RunMeta } from '../routines.js';
|
|
16
|
+
/**
|
|
17
|
+
* A parsed GitHub webhook: the event name (from the `X-GitHub-Event` HTTP
|
|
18
|
+
* header) plus the decoded JSON body. Kept deliberately loose (`payload` is an
|
|
19
|
+
* arbitrary object) because callers may hand us any of GitHub's event shapes.
|
|
20
|
+
*/
|
|
21
|
+
export interface GithubWebhook {
|
|
22
|
+
/** The GitHub event name, e.g. `pull_request`, `push`, `issue_comment`. */
|
|
23
|
+
event: string;
|
|
24
|
+
/** The decoded JSON request body. */
|
|
25
|
+
payload: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
/** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
|
|
28
|
+
export declare function webhookRepo(payload: Record<string, unknown>): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Extract every candidate branch a webhook payload references, per event type.
|
|
31
|
+
* A trigger's `branch` matches if it equals any of these. Different events
|
|
32
|
+
* carry the branch in different places:
|
|
33
|
+
* - push: `ref` (refs/heads/<b>)
|
|
34
|
+
* - pull_request: base + head refs of the PR
|
|
35
|
+
* - workflow_run: `workflow_run.head_branch`
|
|
36
|
+
* - issue_comment: no branch (comments aren't branch-scoped)
|
|
37
|
+
*/
|
|
38
|
+
export declare function webhookBranches(event: string, payload: Record<string, unknown>): string[];
|
|
39
|
+
/** True when a single job's trigger matches the given webhook. Pure. */
|
|
40
|
+
export declare function jobMatchesWebhook(job: JobConfig, webhook: GithubWebhook): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Pure matcher: given a set of jobs and an incoming webhook, return the jobs
|
|
43
|
+
* whose `trigger` matches (event + optional repo + optional branch). Jobs
|
|
44
|
+
* without a trigger (schedule-only routines) are never selected — proving
|
|
45
|
+
* time-based jobs are unaffected by webhook delivery.
|
|
46
|
+
*/
|
|
47
|
+
export declare function matchJobsToWebhook(jobs: JobConfig[], webhook: GithubWebhook): JobConfig[];
|
|
48
|
+
/** Options for firing webhook-matched jobs (dispatch is injectable for tests). */
|
|
49
|
+
export interface FireWebhookOptions {
|
|
50
|
+
/** Job source. Defaults to all persisted routines (`listJobs()`). */
|
|
51
|
+
jobs?: JobConfig[];
|
|
52
|
+
/**
|
|
53
|
+
* How to dispatch a matched job. Defaults to `executeJobDetached` — the SAME
|
|
54
|
+
* path a cron fire uses (see `daemon.ts`). Injectable so tests can assert
|
|
55
|
+
* matching without spawning real agent processes.
|
|
56
|
+
*/
|
|
57
|
+
dispatch?: (config: JobConfig) => Promise<RunMeta>;
|
|
58
|
+
}
|
|
59
|
+
/** Result of firing one matched job. */
|
|
60
|
+
export interface FiredJob {
|
|
61
|
+
jobName: string;
|
|
62
|
+
runId: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Match an incoming webhook against the persisted routines and fire each match
|
|
66
|
+
* through the cron dispatch path. Returns one entry per fired job.
|
|
67
|
+
*/
|
|
68
|
+
export declare function fireWebhookJobs(webhook: GithubWebhook, options?: FireWebhookOptions): Promise<FiredJob[]>;
|
|
69
|
+
/** Options for the local webhook http listener. */
|
|
70
|
+
export interface WebhookServerOptions {
|
|
71
|
+
port?: number;
|
|
72
|
+
host?: string;
|
|
73
|
+
/** Override the fire options (mainly for tests). */
|
|
74
|
+
fire?: FireWebhookOptions;
|
|
75
|
+
/** Called after each delivery is handled (mainly for tests/observability). */
|
|
76
|
+
onDelivery?: (event: string, fired: FiredJob[]) => void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Start a minimal local webhook receiver. POSTs are read as JSON, the GitHub
|
|
80
|
+
* event is taken from the `X-GitHub-Event` header, and matching routines are
|
|
81
|
+
* fired via {@link fireWebhookJobs}. Returns the underlying server so callers
|
|
82
|
+
* can `close()` it. The heavy lifting is the pure matcher above; this is only
|
|
83
|
+
* the transport.
|
|
84
|
+
*/
|
|
85
|
+
export declare function startWebhookServer(options?: WebhookServerOptions): http.Server;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub webhook trigger receiver for routines.
|
|
3
|
+
*
|
|
4
|
+
* A routine may declare a `trigger` block instead of (or alongside) a cron
|
|
5
|
+
* `schedule` (see `JobConfig.trigger` in `../routines.ts`). This module turns
|
|
6
|
+
* an incoming GitHub webhook into the set of routines it should fire, and
|
|
7
|
+
* dispatches those routines through the exact same path a cron fire uses
|
|
8
|
+
* (`executeJobDetached`).
|
|
9
|
+
*
|
|
10
|
+
* The matching logic (`matchJobsToWebhook`) is a pure, side-effect-free
|
|
11
|
+
* function so it can be unit-tested without a running daemon or http server.
|
|
12
|
+
* The optional http listener (`startWebhookServer`) is a thin adapter over it.
|
|
13
|
+
*/
|
|
14
|
+
import * as http from 'http';
|
|
15
|
+
import { listJobs } from '../routines.js';
|
|
16
|
+
import { executeJobDetached } from '../runner.js';
|
|
17
|
+
/** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
|
|
18
|
+
export function webhookRepo(payload) {
|
|
19
|
+
const repo = payload?.repository;
|
|
20
|
+
const fullName = repo?.full_name;
|
|
21
|
+
return typeof fullName === 'string' && fullName.length > 0 ? fullName : null;
|
|
22
|
+
}
|
|
23
|
+
/** Strip a `refs/heads/` (or `refs/tags/`) prefix to the short branch/tag name. */
|
|
24
|
+
function shortRef(ref) {
|
|
25
|
+
return ref.replace(/^refs\/(heads|tags)\//, '');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract every candidate branch a webhook payload references, per event type.
|
|
29
|
+
* A trigger's `branch` matches if it equals any of these. Different events
|
|
30
|
+
* carry the branch in different places:
|
|
31
|
+
* - push: `ref` (refs/heads/<b>)
|
|
32
|
+
* - pull_request: base + head refs of the PR
|
|
33
|
+
* - workflow_run: `workflow_run.head_branch`
|
|
34
|
+
* - issue_comment: no branch (comments aren't branch-scoped)
|
|
35
|
+
*/
|
|
36
|
+
export function webhookBranches(event, payload) {
|
|
37
|
+
const branches = new Set();
|
|
38
|
+
const add = (v) => {
|
|
39
|
+
if (typeof v === 'string' && v.length > 0)
|
|
40
|
+
branches.add(shortRef(v));
|
|
41
|
+
};
|
|
42
|
+
switch (event) {
|
|
43
|
+
case 'push':
|
|
44
|
+
add(payload.ref);
|
|
45
|
+
break;
|
|
46
|
+
case 'pull_request': {
|
|
47
|
+
const pr = payload.pull_request;
|
|
48
|
+
add(pr?.base?.ref);
|
|
49
|
+
add(pr?.head?.ref);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case 'workflow_run': {
|
|
53
|
+
const run = payload.workflow_run;
|
|
54
|
+
add(run?.head_branch);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
default:
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
return [...branches];
|
|
61
|
+
}
|
|
62
|
+
/** True when a single job's trigger matches the given webhook. Pure. */
|
|
63
|
+
export function jobMatchesWebhook(job, webhook) {
|
|
64
|
+
const trigger = job.trigger;
|
|
65
|
+
if (!trigger || trigger.type !== 'github_event')
|
|
66
|
+
return false;
|
|
67
|
+
if (trigger.event !== webhook.event)
|
|
68
|
+
return false;
|
|
69
|
+
if (trigger.repo) {
|
|
70
|
+
const repo = webhookRepo(webhook.payload);
|
|
71
|
+
if (!repo || repo.toLowerCase() !== trigger.repo.toLowerCase())
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
if (trigger.branch) {
|
|
75
|
+
const branches = webhookBranches(webhook.event, webhook.payload);
|
|
76
|
+
if (!branches.some((b) => b === trigger.branch))
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Pure matcher: given a set of jobs and an incoming webhook, return the jobs
|
|
83
|
+
* whose `trigger` matches (event + optional repo + optional branch). Jobs
|
|
84
|
+
* without a trigger (schedule-only routines) are never selected — proving
|
|
85
|
+
* time-based jobs are unaffected by webhook delivery.
|
|
86
|
+
*/
|
|
87
|
+
export function matchJobsToWebhook(jobs, webhook) {
|
|
88
|
+
return jobs.filter((job) => job.enabled !== false && jobMatchesWebhook(job, webhook));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Match an incoming webhook against the persisted routines and fire each match
|
|
92
|
+
* through the cron dispatch path. Returns one entry per fired job.
|
|
93
|
+
*/
|
|
94
|
+
export async function fireWebhookJobs(webhook, options = {}) {
|
|
95
|
+
const jobs = options.jobs ?? listJobs();
|
|
96
|
+
const dispatch = options.dispatch ?? executeJobDetached;
|
|
97
|
+
const matched = matchJobsToWebhook(jobs, webhook);
|
|
98
|
+
const fired = [];
|
|
99
|
+
for (const job of matched) {
|
|
100
|
+
const meta = await dispatch(job);
|
|
101
|
+
fired.push({ jobName: job.name, runId: meta.runId });
|
|
102
|
+
}
|
|
103
|
+
return fired;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Start a minimal local webhook receiver. POSTs are read as JSON, the GitHub
|
|
107
|
+
* event is taken from the `X-GitHub-Event` header, and matching routines are
|
|
108
|
+
* fired via {@link fireWebhookJobs}. Returns the underlying server so callers
|
|
109
|
+
* can `close()` it. The heavy lifting is the pure matcher above; this is only
|
|
110
|
+
* the transport.
|
|
111
|
+
*/
|
|
112
|
+
export function startWebhookServer(options = {}) {
|
|
113
|
+
const server = http.createServer((req, res) => {
|
|
114
|
+
if (req.method !== 'POST') {
|
|
115
|
+
res.writeHead(405, { 'content-type': 'text/plain' });
|
|
116
|
+
res.end('method not allowed');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const event = req.headers['x-github-event'] ?? '';
|
|
120
|
+
const chunks = [];
|
|
121
|
+
req.on('data', (c) => chunks.push(c));
|
|
122
|
+
req.on('end', () => {
|
|
123
|
+
void (async () => {
|
|
124
|
+
try {
|
|
125
|
+
const raw = Buffer.concat(chunks).toString('utf-8');
|
|
126
|
+
const payload = raw ? JSON.parse(raw) : {};
|
|
127
|
+
const fired = await fireWebhookJobs({ event, payload }, options.fire);
|
|
128
|
+
options.onDelivery?.(event, fired);
|
|
129
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
130
|
+
res.end(JSON.stringify({ ok: true, fired: fired.map((f) => f.jobName) }));
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
res.writeHead(400, { 'content-type': 'application/json' });
|
|
134
|
+
res.end(JSON.stringify({ ok: false, error: err.message }));
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
server.listen(options.port ?? 0, options.host ?? '127.0.0.1');
|
|
140
|
+
return server;
|
|
141
|
+
}
|
package/dist/lib/versions.d.ts
CHANGED
|
@@ -121,6 +121,12 @@ export declare function getBinaryPath(agent: AgentId, version: string): string;
|
|
|
121
121
|
export declare function getVersionHomePath(agent: AgentId, version: string): string;
|
|
122
122
|
/**
|
|
123
123
|
* Check if a specific version is installed.
|
|
124
|
+
*
|
|
125
|
+
* Probes the actual launch binary (the same executable the shims run), not just
|
|
126
|
+
* the version dir or the node_modules/.bin/<cli> wrapper. For npm agents that
|
|
127
|
+
* means statting the package's real `bin` target (getPackageBinaryPath), so a
|
|
128
|
+
* present-but-gutted install (vendor auto-updater destroyed the binary) reports
|
|
129
|
+
* as NOT installed and `agents add` re-runs its install step to repair it.
|
|
124
130
|
*/
|
|
125
131
|
export declare function isVersionInstalled(agent: AgentId, version: string): boolean;
|
|
126
132
|
/**
|
|
@@ -346,6 +352,23 @@ export declare function listRepoNames(): string[];
|
|
|
346
352
|
* exactly as an unscoped full reconcile does.
|
|
347
353
|
*/
|
|
348
354
|
export declare function buildRepoScopedSelection(repo: string, cwd?: string): ResourceSelection;
|
|
355
|
+
/**
|
|
356
|
+
* Union several ResourceSelections into one, deduping names per kind. Pure — no
|
|
357
|
+
* FS — so the merge logic is unit-tested without a DotAgents layout. `memory` is
|
|
358
|
+
* a whole-file merge of the user+system layers, not a per-repo artifact: pass
|
|
359
|
+
* `includeMemory` to request a full memory write (`'all'`), else the skip
|
|
360
|
+
* sentinel (`[]`) is kept.
|
|
361
|
+
*/
|
|
362
|
+
export declare function unionResourceSelections(selections: ResourceSelection[], includeMemory: boolean): ResourceSelection;
|
|
363
|
+
/**
|
|
364
|
+
* Build one selection spanning several DotAgent repos — the source set an
|
|
365
|
+
* interactive `agents sync` picker collects. Each repo is scoped via
|
|
366
|
+
* `buildRepoScopedSelection`, then unioned. Memory is written whole when the
|
|
367
|
+
* `user` or `system` layer is among the selected repos (that's where the memory
|
|
368
|
+
* file's content comes from); a project-only / alias-only pick leaves it
|
|
369
|
+
* untouched.
|
|
370
|
+
*/
|
|
371
|
+
export declare function mergeRepoScopedSelections(repos: string[], cwd?: string): ResourceSelection;
|
|
349
372
|
/**
|
|
350
373
|
* Sync central resources (~/.agents/) into a specific version's config directory.
|
|
351
374
|
* Copies selected resources from central storage into {versionHome}/.{agent}/.
|
package/dist/lib/versions.js
CHANGED
|
@@ -34,6 +34,7 @@ import { parseMcpServerConfig } from './mcp.js';
|
|
|
34
34
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
35
35
|
import { importInstallScriptBinary } from './import.js';
|
|
36
36
|
import { IS_WINDOWS } from './platform/index.js';
|
|
37
|
+
import { pruneVersionHomeHookEntriesFromSettings } from './hooks.js';
|
|
37
38
|
import { supports, explainSkip } from './capabilities.js';
|
|
38
39
|
import { discoverPlugins } from './plugins.js';
|
|
39
40
|
import { loadManifest, saveManifest, buildManifest as buildSyncManifest, isStale } from './staleness/index.js';
|
|
@@ -840,12 +841,65 @@ export function getBinaryPath(agent, version) {
|
|
|
840
841
|
export function getVersionHomePath(agent, version) {
|
|
841
842
|
return path.join(getVersionDir(agent, version), 'home');
|
|
842
843
|
}
|
|
844
|
+
/**
|
|
845
|
+
* Resolve the REAL launch binary for an npm-package agent version: the file the
|
|
846
|
+
* installed package's `bin` entry points to — e.g.
|
|
847
|
+
* node_modules/@anthropic-ai/claude-code/bin/claude.exe on Windows. This is the
|
|
848
|
+
* executable that the node_modules/.bin/<cli>.cmd wrapper ultimately execs (and
|
|
849
|
+
* what `agents run` spawns), NOT the wrapper itself.
|
|
850
|
+
*
|
|
851
|
+
* The distinction is load-bearing: npm leaves the tiny node_modules/.bin/<cli>
|
|
852
|
+
* and <cli>.cmd wrappers in place even after a vendor auto-updater destroys the
|
|
853
|
+
* multi-hundred-MB real binary the wrapper points at. Keying "installed" on the
|
|
854
|
+
* wrapper (getBinaryPath) — or on the version dir — therefore reports a gutted
|
|
855
|
+
* install as healthy, and `agents run` then dies at spawn with
|
|
856
|
+
* "'...claude.exe' is not recognized".
|
|
857
|
+
*
|
|
858
|
+
* Returns null for agents without an npm package (grok/droid/installScript,
|
|
859
|
+
* whose getBinaryPath already resolves the real per-host binary) or when the
|
|
860
|
+
* installed package.json can't be read/parsed — callers fall back to the
|
|
861
|
+
* generic getBinaryPath check in those cases.
|
|
862
|
+
*/
|
|
863
|
+
function getPackageBinaryPath(agent, version) {
|
|
864
|
+
const agentConfig = AGENTS[agent];
|
|
865
|
+
if (!agentConfig.npmPackage)
|
|
866
|
+
return null;
|
|
867
|
+
const pkgRoot = path.join(getVersionDir(agent, version), 'node_modules', agentConfig.npmPackage);
|
|
868
|
+
let bin;
|
|
869
|
+
try {
|
|
870
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'));
|
|
871
|
+
bin = pkg.bin;
|
|
872
|
+
}
|
|
873
|
+
catch {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
let rel;
|
|
877
|
+
if (typeof bin === 'string') {
|
|
878
|
+
rel = bin;
|
|
879
|
+
}
|
|
880
|
+
else if (bin && typeof bin === 'object') {
|
|
881
|
+
const map = bin;
|
|
882
|
+
// Prefer the entry named after our launch command; else the first bin.
|
|
883
|
+
rel = map[agentConfig.cliCommand] ?? Object.values(map)[0];
|
|
884
|
+
}
|
|
885
|
+
if (!rel || typeof rel !== 'string')
|
|
886
|
+
return null;
|
|
887
|
+
return path.join(pkgRoot, rel);
|
|
888
|
+
}
|
|
843
889
|
/**
|
|
844
890
|
* Check if a specific version is installed.
|
|
891
|
+
*
|
|
892
|
+
* Probes the actual launch binary (the same executable the shims run), not just
|
|
893
|
+
* the version dir or the node_modules/.bin/<cli> wrapper. For npm agents that
|
|
894
|
+
* means statting the package's real `bin` target (getPackageBinaryPath), so a
|
|
895
|
+
* present-but-gutted install (vendor auto-updater destroyed the binary) reports
|
|
896
|
+
* as NOT installed and `agents add` re-runs its install step to repair it.
|
|
845
897
|
*/
|
|
846
898
|
export function isVersionInstalled(agent, version) {
|
|
847
|
-
const
|
|
848
|
-
|
|
899
|
+
const packageBinary = getPackageBinaryPath(agent, version);
|
|
900
|
+
if (packageBinary !== null)
|
|
901
|
+
return fs.existsSync(packageBinary);
|
|
902
|
+
return fs.existsSync(getBinaryPath(agent, version));
|
|
849
903
|
}
|
|
850
904
|
/**
|
|
851
905
|
* Get the latest available version from npm for an agent.
|
|
@@ -936,8 +990,10 @@ export function listInstalledVersions(agent) {
|
|
|
936
990
|
const versions = [];
|
|
937
991
|
for (const entry of entries) {
|
|
938
992
|
if (entry.isDirectory()) {
|
|
939
|
-
|
|
940
|
-
|
|
993
|
+
// Probe the real launch binary (isVersionInstalled), not just the
|
|
994
|
+
// node_modules/.bin wrapper — a gutted install must not count as healthy
|
|
995
|
+
// in the balanced account/version picker.
|
|
996
|
+
if (isVersionInstalled(agent, entry.name)) {
|
|
941
997
|
versions.push(entry.name);
|
|
942
998
|
}
|
|
943
999
|
}
|
|
@@ -966,7 +1022,7 @@ export function listInstalledVersionDirs(agent) {
|
|
|
966
1022
|
continue;
|
|
967
1023
|
out.push({
|
|
968
1024
|
version: entry.name,
|
|
969
|
-
hasBinary:
|
|
1025
|
+
hasBinary: isVersionInstalled(agent, entry.name),
|
|
970
1026
|
});
|
|
971
1027
|
}
|
|
972
1028
|
return out.sort((a, b) => compareVersions(a.version, b.version));
|
|
@@ -1117,7 +1173,7 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
1117
1173
|
};
|
|
1118
1174
|
}
|
|
1119
1175
|
onProgress?.(`Installing ${packageSpec}...`);
|
|
1120
|
-
const { stdout } = await execFileAsync('npm', ['install', packageSpec], { cwd: versionDir, shell: winShell });
|
|
1176
|
+
const { stdout } = await execFileAsync('npm', ['install', packageSpec, '--ignore-scripts'], { cwd: versionDir, shell: winShell });
|
|
1121
1177
|
// Determine the actual installed version
|
|
1122
1178
|
let installedVersion = version;
|
|
1123
1179
|
if (version === 'latest') {
|
|
@@ -1311,16 +1367,20 @@ export function removeVersion(agent, version) {
|
|
|
1311
1367
|
}
|
|
1312
1368
|
// Remove versioned alias (e.g., claude@2.0.65)
|
|
1313
1369
|
removeVersionedAlias(agent, version);
|
|
1314
|
-
//
|
|
1370
|
+
// If the removed version was the global default, keep launchers working:
|
|
1371
|
+
// reassign the default to the newest remaining version, or clear it outright
|
|
1372
|
+
// only when nothing is left. Leaving a dangling default pointer here is what
|
|
1373
|
+
// broke every launcher after removing the pinned default.
|
|
1315
1374
|
if (getGlobalDefault(agent) === version) {
|
|
1316
|
-
const meta = readMeta();
|
|
1317
|
-
if (meta.agents?.[agent]) {
|
|
1318
|
-
delete meta.agents[agent];
|
|
1319
|
-
writeMeta(meta);
|
|
1320
|
-
}
|
|
1321
1375
|
const remaining = listInstalledVersions(agent);
|
|
1322
1376
|
if (remaining.length > 0) {
|
|
1323
|
-
|
|
1377
|
+
const newestRemaining = remaining[remaining.length - 1];
|
|
1378
|
+
setGlobalDefault(agent, newestRemaining);
|
|
1379
|
+
console.log(chalk.yellow(`Default ${agent} was ${version} (removed); reassigned to ${newestRemaining}. Change it with: agents use ${agent}@<version>`));
|
|
1380
|
+
}
|
|
1381
|
+
else {
|
|
1382
|
+
setGlobalDefault(agent, undefined);
|
|
1383
|
+
console.log(chalk.yellow(`Removed the last installed ${agent} version and cleared its default. Reinstall with: agents add ${agent}, then set one with: agents use ${agent}@<version>`));
|
|
1324
1384
|
}
|
|
1325
1385
|
}
|
|
1326
1386
|
// Clean up dangling config symlink if it pointed to the removed version
|
|
@@ -1334,6 +1394,18 @@ export function removeVersion(agent, version) {
|
|
|
1334
1394
|
// Ignore if already gone
|
|
1335
1395
|
}
|
|
1336
1396
|
}
|
|
1397
|
+
// Clear dead per-version hook entries the removed version left behind in the
|
|
1398
|
+
// remaining versions' settings. Their command paths point under the now-gone
|
|
1399
|
+
// version home, so they error on every tool call until the next sync. Scoped
|
|
1400
|
+
// to the Claude-family settings.json format (claude + droid) — the surface
|
|
1401
|
+
// where per-version guard hooks accumulate; other agents self-heal on sync.
|
|
1402
|
+
if (agent === 'claude' || agent === 'droid') {
|
|
1403
|
+
const configDir = agentConfigDirName(agent);
|
|
1404
|
+
for (const remaining of listInstalledVersions(agent)) {
|
|
1405
|
+
const settingsPath = path.join(getVersionHomePath(agent, remaining), configDir, 'settings.json');
|
|
1406
|
+
pruneVersionHomeHookEntriesFromSettings(settingsPath, agent, version);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1337
1409
|
emit('version.remove', { agent, version });
|
|
1338
1410
|
return true;
|
|
1339
1411
|
}
|
|
@@ -1715,6 +1787,40 @@ export function buildRepoScopedSelection(repo, cwd = process.cwd()) {
|
|
|
1715
1787
|
selection.memory = 'all';
|
|
1716
1788
|
return selection;
|
|
1717
1789
|
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Union several ResourceSelections into one, deduping names per kind. Pure — no
|
|
1792
|
+
* FS — so the merge logic is unit-tested without a DotAgents layout. `memory` is
|
|
1793
|
+
* a whole-file merge of the user+system layers, not a per-repo artifact: pass
|
|
1794
|
+
* `includeMemory` to request a full memory write (`'all'`), else the skip
|
|
1795
|
+
* sentinel (`[]`) is kept.
|
|
1796
|
+
*/
|
|
1797
|
+
export function unionResourceSelections(selections, includeMemory) {
|
|
1798
|
+
const merged = {};
|
|
1799
|
+
const kinds = ['commands', 'skills', 'hooks', 'subagents', 'permissions', 'mcp', 'plugins', 'workflows'];
|
|
1800
|
+
for (const sel of selections) {
|
|
1801
|
+
for (const kind of kinds) {
|
|
1802
|
+
const names = sel[kind];
|
|
1803
|
+
if (!Array.isArray(names) || names.length === 0)
|
|
1804
|
+
continue;
|
|
1805
|
+
const existing = merged[kind] ?? [];
|
|
1806
|
+
merged[kind] = Array.from(new Set([...existing, ...names]));
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
merged.memory = includeMemory ? 'all' : [];
|
|
1810
|
+
return merged;
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Build one selection spanning several DotAgent repos — the source set an
|
|
1814
|
+
* interactive `agents sync` picker collects. Each repo is scoped via
|
|
1815
|
+
* `buildRepoScopedSelection`, then unioned. Memory is written whole when the
|
|
1816
|
+
* `user` or `system` layer is among the selected repos (that's where the memory
|
|
1817
|
+
* file's content comes from); a project-only / alias-only pick leaves it
|
|
1818
|
+
* untouched.
|
|
1819
|
+
*/
|
|
1820
|
+
export function mergeRepoScopedSelections(repos, cwd = process.cwd()) {
|
|
1821
|
+
const includeMemory = repos.includes('user') || repos.includes('system');
|
|
1822
|
+
return unionResourceSelections(repos.map((r) => buildRepoScopedSelection(r, cwd)), includeMemory);
|
|
1823
|
+
}
|
|
1718
1824
|
/**
|
|
1719
1825
|
* Sync central resources (~/.agents/) into a specific version's config directory.
|
|
1720
1826
|
* Copies selected resources from central storage into {versionHome}/.{agent}/.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Default watchdog thresholds, mirrored from the Swarmify VS Code runtime. */
|
|
2
|
+
export declare const WATCHDOG_TAIL_LINES = 20;
|
|
3
|
+
export declare const WATCHDOG_STALL_MS = 300000;
|
|
4
|
+
export declare const WATCHDOG_COOLDOWN_MS = 1200000;
|
|
5
|
+
export declare const WATCHDOG_DORMANT_MS = 3600000;
|
|
6
|
+
/**
|
|
7
|
+
* Read the last `maxLines` non-empty lines of a JSONL transcript by seeking
|
|
8
|
+
* backward from EOF in 64KB chunks. A tail that begins mid-line yields one
|
|
9
|
+
* malformed leading line, which the callers' per-line JSON try/catch tolerates.
|
|
10
|
+
* Returns `[]` on any read error or empty file.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readTailLines(filePath: string, maxLines: number): string[];
|
|
13
|
+
/**
|
|
14
|
+
* Given candidate transcript-root directories, find the transcript file for a
|
|
15
|
+
* session. Handles the flat Claude layout (`<sessionId>.jsonl` inside a
|
|
16
|
+
* per-project `<enc>/` subfolder), Codex-style names that merely embed the uuid
|
|
17
|
+
* (`rollout-…-<sessionId>.jsonl`), and — critically — the DEEP date partitions
|
|
18
|
+
* Codex/Droid use (`sessions/YYYY/MM/DD/rollout-…-<uuid>.jsonl`). The scan is
|
|
19
|
+
* fully recursive via `walkForFiles` (the same walker session/discover.ts uses),
|
|
20
|
+
* so no fixed number of subdir levels is assumed — a hand-rolled one-level scan
|
|
21
|
+
* silently missed every Codex transcript. Newest mtime wins when a uuid appears
|
|
22
|
+
* in more than one root (e.g. across version homes). Pure over its `dirs`
|
|
23
|
+
* argument, so it is testable without touching the real home directory.
|
|
24
|
+
*/
|
|
25
|
+
export declare function findSessionJsonlIn(dirs: string[], sessionId: string, ext?: string): string | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a session transcript path from its id + agent, reusing the CLI's
|
|
28
|
+
* per-version transcript roots. Returns `undefined` if no transcript is found.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveWatchdogSessionPath(sessionId: string, agent: string): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Read the last `maxLines` JSONL lines for a session by id + agent. Returns
|
|
33
|
+
* `[]` when the transcript cannot be located or read.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readWatchdogTail(sessionId: string, agent: string, maxLines?: number): string[];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Watchdog tail reader: locate a session transcript from its id + agent and
|
|
2
|
+
// pull the last N raw JSONL lines. The raw lines feed the pure detectors in
|
|
3
|
+
// watchdog.ts (isLikelyTrulyBlocked / renderWatchdogPrompt) and the pure
|
|
4
|
+
// summarizer in watchdogTail.ts.
|
|
5
|
+
//
|
|
6
|
+
// Adapted from Swarmify's readTailLines (extension/src/vscode/sessions.vscode.ts)
|
|
7
|
+
// to agents-cli's session layout. Path resolution REUSES getAgentSessionDirs()
|
|
8
|
+
// from src/lib/session/discover.ts — the same resolver the rest of the CLI uses
|
|
9
|
+
// to enumerate per-version transcript roots — instead of hardcoding
|
|
10
|
+
// ~/.agents/.history/versions/<agent>/.../projects/<enc>/<sessionId>.jsonl.
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { getAgentSessionDirs } from '../session/discover.js';
|
|
14
|
+
import { walkForFiles } from '../fs-walk.js';
|
|
15
|
+
/** Default watchdog thresholds, mirrored from the Swarmify VS Code runtime. */
|
|
16
|
+
export const WATCHDOG_TAIL_LINES = 20;
|
|
17
|
+
export const WATCHDOG_STALL_MS = 300_000; // 5m — stallSeconds default
|
|
18
|
+
export const WATCHDOG_COOLDOWN_MS = 1_200_000; // 20m — cooldownSeconds default
|
|
19
|
+
export const WATCHDOG_DORMANT_MS = 3_600_000; // 1h — DORMANT_MS
|
|
20
|
+
const CHUNK_SIZE = 64 * 1024;
|
|
21
|
+
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
22
|
+
/** Cap on files walked per transcript root — matches session/discover.ts. */
|
|
23
|
+
const WATCHDOG_WALK_CAP = 100_000;
|
|
24
|
+
/**
|
|
25
|
+
* Per-agent transcript layout, mirroring the `getAgentSessionDirs()` call sites
|
|
26
|
+
* in session/discover.ts so the resolver never diverges from the rest of the CLI:
|
|
27
|
+
* Claude keeps per-project folders under `projects/` (discover.ts:486); Codex and
|
|
28
|
+
* Droid date-/work-partition rollout `.jsonl` deep under `sessions/`
|
|
29
|
+
* (discover.ts:665, :1673); Gemini nests chat `.json` under `tmp/<hash>/chats/`
|
|
30
|
+
* (discover.ts:820). Subdir + extension are driven from this table instead of the
|
|
31
|
+
* old hardcoded `codex ? 'sessions' : 'projects'` ternary, which sent every
|
|
32
|
+
* non-Codex agent (Gemini included) to the wrong root.
|
|
33
|
+
*/
|
|
34
|
+
const WATCHDOG_SESSION_LAYOUT = {
|
|
35
|
+
claude: { subdir: 'projects', ext: '.jsonl' },
|
|
36
|
+
codex: { subdir: 'sessions', ext: '.jsonl' },
|
|
37
|
+
droid: { subdir: 'sessions', ext: '.jsonl' },
|
|
38
|
+
gemini: { subdir: 'tmp', ext: '.json' },
|
|
39
|
+
};
|
|
40
|
+
const WATCHDOG_SESSION_LAYOUT_DEFAULT = { subdir: 'projects', ext: '.jsonl' };
|
|
41
|
+
/**
|
|
42
|
+
* Read the last `maxLines` non-empty lines of a JSONL transcript by seeking
|
|
43
|
+
* backward from EOF in 64KB chunks. A tail that begins mid-line yields one
|
|
44
|
+
* malformed leading line, which the callers' per-line JSON try/catch tolerates.
|
|
45
|
+
* Returns `[]` on any read error or empty file.
|
|
46
|
+
*/
|
|
47
|
+
export function readTailLines(filePath, maxLines) {
|
|
48
|
+
let fd;
|
|
49
|
+
try {
|
|
50
|
+
fd = fs.openSync(filePath, 'r');
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const fileSize = fs.fstatSync(fd).size;
|
|
57
|
+
if (fileSize === 0)
|
|
58
|
+
return [];
|
|
59
|
+
let position = fileSize;
|
|
60
|
+
let buffer = '';
|
|
61
|
+
let collected = [];
|
|
62
|
+
while (position > 0 && collected.length <= maxLines) {
|
|
63
|
+
const readSize = Math.min(CHUNK_SIZE, position);
|
|
64
|
+
position -= readSize;
|
|
65
|
+
const chunk = Buffer.alloc(readSize);
|
|
66
|
+
fs.readSync(fd, chunk, 0, readSize, position);
|
|
67
|
+
buffer = chunk.toString('utf-8') + buffer;
|
|
68
|
+
collected = buffer.split(/\r?\n/).filter((l) => l.trim());
|
|
69
|
+
}
|
|
70
|
+
return collected.slice(-maxLines);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
try {
|
|
77
|
+
fs.closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* fd already gone */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Given candidate transcript-root directories, find the transcript file for a
|
|
86
|
+
* session. Handles the flat Claude layout (`<sessionId>.jsonl` inside a
|
|
87
|
+
* per-project `<enc>/` subfolder), Codex-style names that merely embed the uuid
|
|
88
|
+
* (`rollout-…-<sessionId>.jsonl`), and — critically — the DEEP date partitions
|
|
89
|
+
* Codex/Droid use (`sessions/YYYY/MM/DD/rollout-…-<uuid>.jsonl`). The scan is
|
|
90
|
+
* fully recursive via `walkForFiles` (the same walker session/discover.ts uses),
|
|
91
|
+
* so no fixed number of subdir levels is assumed — a hand-rolled one-level scan
|
|
92
|
+
* silently missed every Codex transcript. Newest mtime wins when a uuid appears
|
|
93
|
+
* in more than one root (e.g. across version homes). Pure over its `dirs`
|
|
94
|
+
* argument, so it is testable without touching the real home directory.
|
|
95
|
+
*/
|
|
96
|
+
export function findSessionJsonlIn(dirs, sessionId, ext = '.jsonl') {
|
|
97
|
+
if (!sessionId)
|
|
98
|
+
return undefined;
|
|
99
|
+
const matches = (name) => {
|
|
100
|
+
if (!name.endsWith(ext))
|
|
101
|
+
return false;
|
|
102
|
+
const stem = name.slice(0, -ext.length);
|
|
103
|
+
if (stem === sessionId)
|
|
104
|
+
return true;
|
|
105
|
+
// Codex embeds the uuid in a longer filename (rollout-<ts>-<uuid>.jsonl).
|
|
106
|
+
return name.includes(sessionId) || (UUID_RE.test(sessionId) && stem.includes(sessionId));
|
|
107
|
+
};
|
|
108
|
+
let best;
|
|
109
|
+
for (const dir of dirs) {
|
|
110
|
+
for (const file of walkForFiles(dir, ext, WATCHDOG_WALK_CAP)) {
|
|
111
|
+
if (!matches(path.basename(file)))
|
|
112
|
+
continue;
|
|
113
|
+
let mtime;
|
|
114
|
+
try {
|
|
115
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!best || mtime > best.mtime)
|
|
121
|
+
best = { file, mtime };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return best?.file;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve a session transcript path from its id + agent, reusing the CLI's
|
|
128
|
+
* per-version transcript roots. Returns `undefined` if no transcript is found.
|
|
129
|
+
*/
|
|
130
|
+
export function resolveWatchdogSessionPath(sessionId, agent) {
|
|
131
|
+
// Subdir + transcript extension are driven per-agent from WATCHDOG_SESSION_LAYOUT
|
|
132
|
+
// (which mirrors the getAgentSessionDirs() convention in session/discover.ts),
|
|
133
|
+
// not a hardcoded `codex ? 'sessions' : 'projects'` — that ternary sent Codex to
|
|
134
|
+
// a root it only scanned one level deep, and every other agent (Gemini included)
|
|
135
|
+
// to the wrong subdir entirely.
|
|
136
|
+
const layout = WATCHDOG_SESSION_LAYOUT[agent] ?? WATCHDOG_SESSION_LAYOUT_DEFAULT;
|
|
137
|
+
const dirs = getAgentSessionDirs(agent, layout.subdir);
|
|
138
|
+
return findSessionJsonlIn(dirs, sessionId, layout.ext);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Read the last `maxLines` JSONL lines for a session by id + agent. Returns
|
|
142
|
+
* `[]` when the transcript cannot be located or read.
|
|
143
|
+
*/
|
|
144
|
+
export function readWatchdogTail(sessionId, agent, maxLines = WATCHDOG_TAIL_LINES) {
|
|
145
|
+
const filePath = resolveWatchdogSessionPath(sessionId, agent);
|
|
146
|
+
if (!filePath)
|
|
147
|
+
return [];
|
|
148
|
+
return readTailLines(filePath, maxLines);
|
|
149
|
+
}
|