@phnx-labs/agents-cli 1.22.33 → 1.22.34
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 +49 -0
- package/README.md +2 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.js +3 -3
- package/dist/commands/browser-sessions-picker.d.ts +16 -0
- package/dist/commands/browser-sessions-picker.js +179 -0
- package/dist/commands/browser.js +9 -4
- package/dist/commands/hosts.js +1 -5
- package/dist/commands/inspect.js +174 -41
- package/dist/commands/message.d.ts +6 -1
- package/dist/commands/message.js +60 -3
- package/dist/commands/sessions.d.ts +54 -4
- package/dist/commands/sessions.js +252 -46
- package/dist/commands/ssh.js +17 -5
- package/dist/commands/teams.d.ts +28 -0
- package/dist/commands/teams.js +148 -13
- package/dist/commands/upgrade.d.ts +7 -0
- package/dist/commands/upgrade.js +10 -0
- package/dist/commands/watchdog.d.ts +2 -0
- package/dist/commands/watchdog.js +112 -27
- package/dist/index.js +51 -59
- package/dist/lib/agents.js +6 -0
- package/dist/lib/browser/sessions-list.d.ts +81 -0
- package/dist/lib/browser/sessions-list.js +179 -4
- package/dist/lib/daemon.js +45 -5
- package/dist/lib/devices/connect.d.ts +33 -0
- package/dist/lib/devices/connect.js +61 -3
- package/dist/lib/devices/doctor-findings.d.ts +4 -2
- package/dist/lib/devices/doctor-findings.js +4 -2
- package/dist/lib/exec.js +63 -6
- package/dist/lib/help.d.ts +3 -2
- package/dist/lib/help.js +4 -0
- package/dist/lib/hosts/dispatch.d.ts +2 -32
- package/dist/lib/hosts/dispatch.js +6 -61
- package/dist/lib/hosts/tasks.d.ts +7 -0
- package/dist/lib/hosts/tasks.js +9 -0
- package/dist/lib/mailbox-target.d.ts +27 -0
- package/dist/lib/mailbox-target.js +21 -0
- package/dist/lib/mcp.d.ts +10 -0
- package/dist/lib/mcp.js +21 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +11 -0
- package/dist/lib/migrate.js +40 -0
- package/dist/lib/project-root.d.ts +47 -0
- package/dist/lib/project-root.js +68 -0
- 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/secrets/agent.d.ts +9 -2
- package/dist/lib/secrets/agent.js +52 -7
- package/dist/lib/secrets/reaper.d.ts +24 -3
- package/dist/lib/secrets/reaper.js +55 -6
- package/dist/lib/session/discover.js +12 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/shims.js +9 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +8 -3
- package/dist/lib/startup/root-command.d.ts +3 -0
- package/dist/lib/startup/root-command.js +10 -0
- package/dist/lib/teams/agents.d.ts +136 -6
- package/dist/lib/teams/agents.js +324 -58
- package/dist/lib/teams/worktree.d.ts +39 -2
- package/dist/lib/teams/worktree.js +60 -4
- package/dist/lib/types.d.ts +11 -0
- package/dist/lib/versions.js +2 -2
- package/dist/lib/watchdog/history.d.ts +20 -0
- package/dist/lib/watchdog/history.js +46 -0
- package/dist/lib/watchdog/log.d.ts +16 -1
- package/dist/lib/watchdog/log.js +82 -2
- package/dist/lib/watchdog/runner.d.ts +12 -0
- package/dist/lib/watchdog/runner.js +20 -0
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ import { promisify } from 'util';
|
|
|
9
9
|
import * as fs from 'fs/promises';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { safeJoin } from '../paths.js';
|
|
12
|
+
import { getMainRepoRoot } from '../git.js';
|
|
12
13
|
const execFileAsync = promisify(execFile);
|
|
13
14
|
const WORKTREE_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
14
15
|
/**
|
|
@@ -106,7 +107,16 @@ export async function localDefaultBranch(gitRoot) {
|
|
|
106
107
|
* code. Matches `createRemoteWorktree` — local and remote isolation share one
|
|
107
108
|
* base policy.
|
|
108
109
|
*
|
|
109
|
-
*
|
|
110
|
+
* Resolves the placement root with {@link getMainRepoRoot}, NOT the local
|
|
111
|
+
* `getGitRoot`/`--show-toplevel`, on purpose: when `repoDir` (the caller's
|
|
112
|
+
* ambient cwd) is itself inside another teammate's linked worktree,
|
|
113
|
+
* `--show-toplevel` returns THAT worktree's own root, so the new worktree
|
|
114
|
+
* lands nested inside it (observed: `.../worktrees/A/.agents/worktrees/B`,
|
|
115
|
+
* destroyed along with A's cleanup). `getMainRepoRoot` follows
|
|
116
|
+
* `--git-common-dir`, which always points at the primary checkout's `.git`
|
|
117
|
+
* regardless of which worktree the caller is standing in.
|
|
118
|
+
*
|
|
119
|
+
* @param repoDir - Directory inside the git repository (main checkout or any linked worktree)
|
|
110
120
|
* @param worktreeName - Name for the worktree (used in path and branch)
|
|
111
121
|
* @returns The absolute path to the created worktree
|
|
112
122
|
*/
|
|
@@ -114,7 +124,7 @@ export async function createWorktree(repoDir, worktreeName) {
|
|
|
114
124
|
if (!WORKTREE_NAME_RE.test(worktreeName)) {
|
|
115
125
|
throw new Error(`Invalid worktree name: ${worktreeName}`);
|
|
116
126
|
}
|
|
117
|
-
const gitRoot = await
|
|
127
|
+
const gitRoot = await getMainRepoRoot(repoDir);
|
|
118
128
|
const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
|
|
119
129
|
const branchName = `agents/${worktreeName}`;
|
|
120
130
|
const base = await localDefaultBranch(gitRoot);
|
|
@@ -137,7 +147,12 @@ export async function createWorktree(repoDir, worktreeName) {
|
|
|
137
147
|
/**
|
|
138
148
|
* Remove a git worktree and optionally its branch.
|
|
139
149
|
*
|
|
140
|
-
*
|
|
150
|
+
* Resolves via {@link getMainRepoRoot}, same as {@link createWorktree} — a
|
|
151
|
+
* caller standing inside a DIFFERENT linked worktree (e.g. a `teams stop`
|
|
152
|
+
* run from within another teammate's checkout) must still target the main
|
|
153
|
+
* repo's `.agents/worktrees/<name>`, not `--show-toplevel`'s local answer.
|
|
154
|
+
*
|
|
155
|
+
* @param repoDir - Directory inside the git repository (main checkout or any linked worktree)
|
|
141
156
|
* @param worktreeName - Name of the worktree to remove
|
|
142
157
|
* @param deleteBranch - Whether to delete the associated branch
|
|
143
158
|
*/
|
|
@@ -145,7 +160,7 @@ export async function removeWorktree(repoDir, worktreeName, deleteBranch = true)
|
|
|
145
160
|
if (!WORKTREE_NAME_RE.test(worktreeName)) {
|
|
146
161
|
throw new Error(`Invalid worktree name: ${worktreeName}`);
|
|
147
162
|
}
|
|
148
|
-
const gitRoot = await
|
|
163
|
+
const gitRoot = await getMainRepoRoot(repoDir);
|
|
149
164
|
const worktreePath = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
|
|
150
165
|
const branchName = `agents/${worktreeName}`;
|
|
151
166
|
try {
|
|
@@ -183,3 +198,44 @@ export function getWorktreePath(gitRoot, worktreeName) {
|
|
|
183
198
|
export function getWorktreeBranch(worktreeName) {
|
|
184
199
|
return `agents/${worktreeName}`;
|
|
185
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* Does the CHECKOUT DIRECTORY for this worktree name exist?
|
|
203
|
+
*
|
|
204
|
+
* Narrower than {@link worktreeExists}, and the difference is load-bearing:
|
|
205
|
+
* `teams add` only ever cleans up after a failed create when there is NO
|
|
206
|
+
* checkout — i.e. when all that can be left is a dangling `agents/<name>`
|
|
207
|
+
* branch ref. That keeps the cleanup incapable of deleting anybody's files,
|
|
208
|
+
* including a concurrent add's freshly-created worktree, whatever the
|
|
209
|
+
* pre-flight probe saw a `git fetch` ago. (RUSH-2356)
|
|
210
|
+
*/
|
|
211
|
+
export async function worktreeCheckoutExists(repoDir, worktreeName) {
|
|
212
|
+
if (!WORKTREE_NAME_RE.test(worktreeName)) {
|
|
213
|
+
throw new Error(`Invalid worktree name: ${worktreeName}`);
|
|
214
|
+
}
|
|
215
|
+
const gitRoot = await getMainRepoRoot(repoDir);
|
|
216
|
+
const dir = safeJoin(path.join(gitRoot, '.agents', 'worktrees'), worktreeName);
|
|
217
|
+
return fs.stat(dir).then(() => true).catch(() => false);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Does anything already exist under this worktree name — the checkout directory
|
|
221
|
+
* or its `agents/<name>` branch?
|
|
222
|
+
*
|
|
223
|
+
* Answered from git and the filesystem, never from teammate records: it is asked
|
|
224
|
+
* by `teams add` immediately BEFORE {@link createWorktree}, so that if the create
|
|
225
|
+
* fails the command can tell "the branch ref I half-created" (safe to remove)
|
|
226
|
+
* from "something that was already here" (never remove — `teams stop` deliberately
|
|
227
|
+
* KEEPS a worktree holding uncommitted changes, and its teammate record is
|
|
228
|
+
* terminal by then, so no record-based check can protect it). (RUSH-2356)
|
|
229
|
+
*/
|
|
230
|
+
export async function worktreeExists(repoDir, worktreeName) {
|
|
231
|
+
if (await worktreeCheckoutExists(repoDir, worktreeName))
|
|
232
|
+
return true;
|
|
233
|
+
const gitRoot = await getMainRepoRoot(repoDir);
|
|
234
|
+
try {
|
|
235
|
+
await execFileAsync('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${getWorktreeBranch(worktreeName)}`], { cwd: gitRoot });
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return false; // `--verify` exits non-zero when the ref doesn't exist
|
|
240
|
+
}
|
|
241
|
+
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -587,6 +587,17 @@ export interface PluginManifest {
|
|
|
587
587
|
description: string;
|
|
588
588
|
version: string;
|
|
589
589
|
agents?: AgentId[];
|
|
590
|
+
/**
|
|
591
|
+
* Who published the plugin, per the official plugin format. Every plugin.json
|
|
592
|
+
* in this repo already carries one; it was missing from this interface, so
|
|
593
|
+
* `loadPluginManifest`'s cast passed it through un-typed and no surface read it.
|
|
594
|
+
* Accepts the shorthand string form as well as the object form.
|
|
595
|
+
*/
|
|
596
|
+
author?: string | {
|
|
597
|
+
name: string;
|
|
598
|
+
email?: string;
|
|
599
|
+
url?: string;
|
|
600
|
+
};
|
|
590
601
|
/** Interactive config fields prompted at install time. Values stored in .user-config.json. */
|
|
591
602
|
userConfig?: PluginUserConfigField[];
|
|
592
603
|
/** Other plugin names this plugin depends on. Missing deps produce a warning. */
|
package/dist/lib/versions.js
CHANGED
|
@@ -31,7 +31,7 @@ import { activeRulesPreset, filterNamesForActiveResourceProfile } from './resour
|
|
|
31
31
|
import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
|
|
32
32
|
import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent, isAgentHardDeprecated, hardDeprecationError } from './agents.js';
|
|
33
33
|
import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
34
|
-
import {
|
|
34
|
+
import { parseMcpConfigForScan, isProjectMcpTrusted } from './mcp.js';
|
|
35
35
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink, assertIsolationBoundary, } from './shims.js';
|
|
36
36
|
import { importInstallScriptBinary } from './import.js';
|
|
37
37
|
import { createInstallation } from './installations/store.js';
|
|
@@ -84,7 +84,7 @@ function getScopedMcpResources(cwd) {
|
|
|
84
84
|
const files = fs.readdirSync(mcpDir)
|
|
85
85
|
.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
86
86
|
for (const file of files) {
|
|
87
|
-
const config =
|
|
87
|
+
const config = parseMcpConfigForScan(path.join(mcpDir, file));
|
|
88
88
|
if (config?.name && !resources.has(config.name)) {
|
|
89
89
|
resources.set(config.name, { name: config.name, scope });
|
|
90
90
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WatchdogEvent, WatchdogEventKind } from './log.js';
|
|
2
|
+
export interface WatchdogHistoryEntry {
|
|
3
|
+
ts: number;
|
|
4
|
+
kind: WatchdogEventKind | 'inspection';
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
agent?: string;
|
|
7
|
+
message: string;
|
|
8
|
+
reason?: string;
|
|
9
|
+
stalledForMs?: number;
|
|
10
|
+
nudgeText?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface WatchdogHistoryOptions {
|
|
13
|
+
limit?: number;
|
|
14
|
+
sinceMs?: number;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
includeTicks?: boolean;
|
|
17
|
+
nowMs?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Select newest history and deliberately remove raw transcript tailLines. */
|
|
20
|
+
export declare function selectWatchdogHistory(events: WatchdogEvent[], options?: WatchdogHistoryOptions): WatchdogHistoryEntry[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Select newest history and deliberately remove raw transcript tailLines. */
|
|
2
|
+
export function selectWatchdogHistory(events, options = {}) {
|
|
3
|
+
const limit = options.limit ?? 50;
|
|
4
|
+
const cutoff = options.sinceMs === undefined
|
|
5
|
+
? Number.NEGATIVE_INFINITY
|
|
6
|
+
: (options.nowMs ?? Date.now()) - options.sinceMs;
|
|
7
|
+
const session = options.sessionId?.toLowerCase();
|
|
8
|
+
const expanded = events.flatMap((event) => {
|
|
9
|
+
const inspections = event.inspections?.map((inspection) => ({
|
|
10
|
+
ts: event.ts,
|
|
11
|
+
kind: 'inspection',
|
|
12
|
+
sessionId: inspection.terminalId,
|
|
13
|
+
agent: inspection.agentType,
|
|
14
|
+
message: inspection.message,
|
|
15
|
+
reason: inspection.reason,
|
|
16
|
+
stalledForMs: inspection.stalledForMs,
|
|
17
|
+
})) ?? [];
|
|
18
|
+
if (event.kind === 'tick' && !options.includeTicks)
|
|
19
|
+
return inspections;
|
|
20
|
+
return [...inspections, {
|
|
21
|
+
ts: event.ts,
|
|
22
|
+
kind: event.kind,
|
|
23
|
+
sessionId: event.terminalId,
|
|
24
|
+
agent: event.agentType,
|
|
25
|
+
message: event.message,
|
|
26
|
+
reason: event.reason,
|
|
27
|
+
stalledForMs: event.stalledForMs,
|
|
28
|
+
nudgeText: event.nudgeText,
|
|
29
|
+
}];
|
|
30
|
+
});
|
|
31
|
+
const matching = expanded
|
|
32
|
+
.filter((entry) => entry.ts >= cutoff)
|
|
33
|
+
.filter((entry) => !session || entry.sessionId?.toLowerCase().startsWith(session))
|
|
34
|
+
.sort((a, b) => b.ts - a.ts);
|
|
35
|
+
if (matching.length <= limit)
|
|
36
|
+
return matching;
|
|
37
|
+
// A busy tick can contain more inspections than the display limit. Keep the
|
|
38
|
+
// newest timeline, but reserve one row for the newest real action.
|
|
39
|
+
const newest = matching.slice(0, limit);
|
|
40
|
+
if (newest.some((entry) => entry.kind !== 'inspection'))
|
|
41
|
+
return newest;
|
|
42
|
+
const newestAction = matching.find((entry) => entry.kind !== 'inspection');
|
|
43
|
+
if (!newestAction)
|
|
44
|
+
return newest;
|
|
45
|
+
return [...newest.slice(0, limit - 1), newestAction].sort((a, b) => b.ts - a.ts);
|
|
46
|
+
}
|
|
@@ -19,7 +19,19 @@ export interface WatchdogEvent {
|
|
|
19
19
|
lastAssistantMessage?: string;
|
|
20
20
|
/** For a 'decision'/'nudge' that injects: the exact text delivered. */
|
|
21
21
|
nudgeText?: string;
|
|
22
|
+
/** Compact per-session evaluations carried by heartbeat ticks. */
|
|
23
|
+
inspections?: WatchdogInspection[];
|
|
22
24
|
}
|
|
25
|
+
export interface WatchdogInspection {
|
|
26
|
+
terminalId?: string;
|
|
27
|
+
agentType: string;
|
|
28
|
+
message: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
stalledForMs?: number;
|
|
31
|
+
}
|
|
32
|
+
/** Parse the audit log without letting a partial final append hide valid rows. */
|
|
33
|
+
export declare function parseWatchdogEvents(text: string): WatchdogEvent[];
|
|
34
|
+
export declare function readWatchdogEvents(logPath?: string): WatchdogEvent[];
|
|
23
35
|
/** Serialize one event to a JSONL line (no trailing newline). */
|
|
24
36
|
export declare function formatEvent(ev: WatchdogEvent): string;
|
|
25
37
|
/**
|
|
@@ -27,7 +39,10 @@ export declare function formatEvent(ev: WatchdogEvent): string;
|
|
|
27
39
|
* writer trims (watchdogLog.ts trimToLast). Large enough to keep a useful
|
|
28
40
|
* history, small enough to bound the file the UI polls.
|
|
29
41
|
*/
|
|
30
|
-
export declare const WATCHDOG_LOG_MAX_LINES =
|
|
42
|
+
export declare const WATCHDOG_LOG_MAX_LINES = 5000;
|
|
43
|
+
export declare const WATCHDOG_TAIL_MAX_CHARS = 4096;
|
|
44
|
+
/** Keep the newest transcript context without letting it consume the audit window. */
|
|
45
|
+
export declare function boundTailLines(lines: string[], maxChars?: number): string[];
|
|
31
46
|
/** Trim a JSONL body to the last `maxLines` events (same logic as the reader). */
|
|
32
47
|
export declare function trimToLast(text: string, maxLines: number): string;
|
|
33
48
|
/**
|
package/dist/lib/watchdog/log.js
CHANGED
|
@@ -18,6 +18,65 @@ import * as path from 'path';
|
|
|
18
18
|
import { withFileLock, atomicWriteFileSync, ensureLockTarget } from '../fs-atomic.js';
|
|
19
19
|
/** Same path the Factory reader pulls (apps/factory/src/core/watchdogLog.ts). */
|
|
20
20
|
export const WATCHDOG_LOG_PATH = path.join(os.homedir(), '.agents', '.cache', 'logs', 'watchdog.log');
|
|
21
|
+
const WATCHDOG_EVENT_KINDS = new Set(['tick', 'decision', 'nudge', 'rotate', 'error']);
|
|
22
|
+
/** Parse the audit log without letting a partial final append hide valid rows. */
|
|
23
|
+
export function parseWatchdogEvents(text) {
|
|
24
|
+
const events = [];
|
|
25
|
+
for (const line of text.split('\n')) {
|
|
26
|
+
if (!line.trim())
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
const value = JSON.parse(line);
|
|
30
|
+
if (typeof value.ts !== 'number' ||
|
|
31
|
+
!Number.isFinite(value.ts) ||
|
|
32
|
+
typeof value.kind !== 'string' ||
|
|
33
|
+
!WATCHDOG_EVENT_KINDS.has(value.kind) ||
|
|
34
|
+
typeof value.message !== 'string')
|
|
35
|
+
continue;
|
|
36
|
+
const inspections = Array.isArray(value.inspections)
|
|
37
|
+
? value.inspections.flatMap((item) => {
|
|
38
|
+
if (item === null || typeof item !== 'object')
|
|
39
|
+
return [];
|
|
40
|
+
const row = item;
|
|
41
|
+
if (typeof row.agentType !== 'string' || typeof row.message !== 'string' || typeof row.reason !== 'string')
|
|
42
|
+
return [];
|
|
43
|
+
return [{
|
|
44
|
+
terminalId: typeof row.terminalId === 'string' ? row.terminalId : undefined,
|
|
45
|
+
agentType: row.agentType,
|
|
46
|
+
message: row.message,
|
|
47
|
+
reason: row.reason,
|
|
48
|
+
stalledForMs: typeof row.stalledForMs === 'number' ? row.stalledForMs : undefined,
|
|
49
|
+
}];
|
|
50
|
+
})
|
|
51
|
+
: undefined;
|
|
52
|
+
events.push({
|
|
53
|
+
ts: value.ts,
|
|
54
|
+
kind: value.kind,
|
|
55
|
+
message: value.message,
|
|
56
|
+
terminalId: typeof value.terminalId === 'string' ? value.terminalId : undefined,
|
|
57
|
+
agentType: typeof value.agentType === 'string' ? value.agentType : undefined,
|
|
58
|
+
reason: typeof value.reason === 'string' ? value.reason : undefined,
|
|
59
|
+
tailLines: Array.isArray(value.tailLines)
|
|
60
|
+
? value.tailLines.filter((item) => typeof item === 'string')
|
|
61
|
+
: undefined,
|
|
62
|
+
stalledForMs: typeof value.stalledForMs === 'number' ? value.stalledForMs : undefined,
|
|
63
|
+
lastUserMessage: typeof value.lastUserMessage === 'string' ? value.lastUserMessage : undefined,
|
|
64
|
+
lastAssistantMessage: typeof value.lastAssistantMessage === 'string' ? value.lastAssistantMessage : undefined,
|
|
65
|
+
nudgeText: typeof value.nudgeText === 'string' ? value.nudgeText : undefined,
|
|
66
|
+
inspections,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Interrupted writers can leave one partial row; earlier rows remain useful.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return events;
|
|
74
|
+
}
|
|
75
|
+
export function readWatchdogEvents(logPath = WATCHDOG_LOG_PATH) {
|
|
76
|
+
if (!fs.existsSync(logPath))
|
|
77
|
+
return [];
|
|
78
|
+
return parseWatchdogEvents(fs.readFileSync(logPath, 'utf8'));
|
|
79
|
+
}
|
|
21
80
|
/** Serialize one event to a JSONL line (no trailing newline). */
|
|
22
81
|
export function formatEvent(ev) {
|
|
23
82
|
return JSON.stringify(ev);
|
|
@@ -27,7 +86,25 @@ export function formatEvent(ev) {
|
|
|
27
86
|
* writer trims (watchdogLog.ts trimToLast). Large enough to keep a useful
|
|
28
87
|
* history, small enough to bound the file the UI polls.
|
|
29
88
|
*/
|
|
30
|
-
export const WATCHDOG_LOG_MAX_LINES =
|
|
89
|
+
export const WATCHDOG_LOG_MAX_LINES = 5000;
|
|
90
|
+
export const WATCHDOG_TAIL_MAX_CHARS = 4096;
|
|
91
|
+
/** Keep the newest transcript context without letting it consume the audit window. */
|
|
92
|
+
export function boundTailLines(lines, maxChars = WATCHDOG_TAIL_MAX_CHARS) {
|
|
93
|
+
const kept = [];
|
|
94
|
+
let remaining = maxChars;
|
|
95
|
+
for (let i = lines.length - 1; i >= 0 && remaining > 0; i--) {
|
|
96
|
+
const line = lines[i];
|
|
97
|
+
if (line.length <= remaining) {
|
|
98
|
+
kept.unshift(line);
|
|
99
|
+
remaining -= line.length;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
kept.unshift(line.slice(-remaining));
|
|
103
|
+
remaining = 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return kept;
|
|
107
|
+
}
|
|
31
108
|
/** Trim a JSONL body to the last `maxLines` events (same logic as the reader). */
|
|
32
109
|
export function trimToLast(text, maxLines) {
|
|
33
110
|
const lines = text.split('\n').filter((l) => l.trim().length > 0);
|
|
@@ -58,7 +135,10 @@ export function appendWatchdogEvents(events, opts = {}) {
|
|
|
58
135
|
catch {
|
|
59
136
|
/* first write — no file yet */
|
|
60
137
|
}
|
|
61
|
-
const
|
|
138
|
+
const boundedEvents = events.map((event) => event.tailLines === undefined
|
|
139
|
+
? event
|
|
140
|
+
: { ...event, tailLines: boundTailLines(event.tailLines) });
|
|
141
|
+
const appended = boundedEvents.map(formatEvent).join('\n') + '\n';
|
|
62
142
|
const body = trimToLast(existing + appended, maxLines);
|
|
63
143
|
atomicWriteFileSync(logPath, body);
|
|
64
144
|
});
|
|
@@ -137,7 +137,19 @@ export interface SessionOutcome {
|
|
|
137
137
|
kind: string;
|
|
138
138
|
host?: string;
|
|
139
139
|
cwd?: string;
|
|
140
|
+
project?: string | null;
|
|
140
141
|
label?: string;
|
|
142
|
+
name?: string;
|
|
143
|
+
topic?: string;
|
|
144
|
+
preview?: string;
|
|
145
|
+
activity?: ActiveSession['activity'];
|
|
146
|
+
status?: ActiveSession['status'];
|
|
147
|
+
startedAtMs?: number;
|
|
148
|
+
lastActivityMs?: number;
|
|
149
|
+
origin?: ActiveSession['origin'];
|
|
150
|
+
routineName?: string;
|
|
151
|
+
machine?: string;
|
|
152
|
+
owner?: string;
|
|
141
153
|
/** classifyTerminal's verdict for this session. */
|
|
142
154
|
stall: StallStatus['kind'];
|
|
143
155
|
/** stalled duration (ms), when stalled. */
|
|
@@ -409,7 +409,19 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
409
409
|
kind: session.kind,
|
|
410
410
|
host: session.host,
|
|
411
411
|
cwd: session.cwd,
|
|
412
|
+
project: session.project,
|
|
412
413
|
label: session.label,
|
|
414
|
+
name: session.name,
|
|
415
|
+
topic: session.topic,
|
|
416
|
+
preview: session.preview,
|
|
417
|
+
activity: session.activity,
|
|
418
|
+
status: session.status,
|
|
419
|
+
startedAtMs: session.startedAtMs,
|
|
420
|
+
lastActivityMs: session.lastActivityMs,
|
|
421
|
+
origin: session.origin,
|
|
422
|
+
routineName: session.routineName,
|
|
423
|
+
machine: session.machine ?? session.provenance?.host,
|
|
424
|
+
owner: session.owner,
|
|
413
425
|
policy,
|
|
414
426
|
stall: 'active',
|
|
415
427
|
decision: 'skip',
|
|
@@ -429,6 +441,7 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
429
441
|
continue;
|
|
430
442
|
}
|
|
431
443
|
const lastActivityMs = lastActivityFor(session);
|
|
444
|
+
base.lastActivityMs = session.lastActivityMs ?? lastActivityMs;
|
|
432
445
|
if (lastActivityMs === undefined) {
|
|
433
446
|
outcomes.push({ ...base, reason: 'no activity timestamp (no transcript / start time)' });
|
|
434
447
|
continue;
|
|
@@ -878,6 +891,13 @@ export async function runWatchdogTick(opts = {}) {
|
|
|
878
891
|
ts: nowMs,
|
|
879
892
|
kind: 'tick',
|
|
880
893
|
message: `${counts.total} live · ${counts.stalled} stalled · ${counts.nudged} nudged · ${counts.unaddressable} un-addressable`,
|
|
894
|
+
inspections: outcomes.map((outcome) => ({
|
|
895
|
+
terminalId: outcome.sessionId,
|
|
896
|
+
agentType: outcome.kind,
|
|
897
|
+
message: outcome.decision,
|
|
898
|
+
reason: outcome.reason,
|
|
899
|
+
stalledForMs: outcome.stalledForMs,
|
|
900
|
+
})),
|
|
881
901
|
});
|
|
882
902
|
appendWatchdogEvents(logEvents, { logPath: opts.logPath });
|
|
883
903
|
return result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.34",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|