@phnx-labs/agents-cli 1.20.76 → 1.20.78
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 +264 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +143 -9
- package/dist/commands/exec.js +87 -19
- package/dist/commands/repo.js +8 -0
- package/dist/commands/routines.js +275 -15
- package/dist/commands/secrets.js +65 -17
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +128 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-picker.d.ts +28 -1
- package/dist/commands/sessions-picker.js +222 -15
- package/dist/commands/sessions.d.ts +117 -4
- package/dist/commands/sessions.js +350 -62
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +48 -3
- package/dist/index.js +5 -6
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/agents.d.ts +0 -21
- package/dist/lib/agents.js +0 -37
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +9 -32
- package/dist/lib/daemon.js +70 -84
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +27 -4
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/git.d.ts +13 -0
- package/dist/lib/git.js +102 -4
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hosts/dispatch.d.ts +9 -0
- package/dist/lib/hosts/dispatch.js +24 -4
- package/dist/lib/hosts/passthrough.js +7 -2
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +33 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +4 -0
- package/dist/lib/hosts/run-target.js +5 -1
- package/dist/lib/hosts/session-index.js +3 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +9 -0
- package/dist/lib/menubar/install-menubar.js +14 -0
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/overdue.d.ts +4 -5
- package/dist/lib/overdue.js +8 -41
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +68 -0
- package/dist/lib/routines.js +74 -7
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +156 -28
- package/dist/lib/sandbox.js +0 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +11 -4
- package/dist/lib/secrets/agent.js +46 -24
- package/dist/lib/secrets/bundles.d.ts +21 -7
- package/dist/lib/secrets/bundles.js +82 -38
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- package/dist/lib/secrets/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +16 -2
- package/dist/lib/secrets/session-store.js +118 -15
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/session/active.d.ts +17 -0
- package/dist/lib/session/active.js +105 -7
- package/dist/lib/session/db.d.ts +43 -0
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +28 -3
- package/dist/lib/session/discover.js +110 -14
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/parse.js +11 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.js +18 -16
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +94 -12
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/state.d.ts +7 -3
- package/dist/lib/state.js +20 -5
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -37,6 +37,13 @@ export interface SessionProvenance {
|
|
|
37
37
|
mux?: MuxLocation;
|
|
38
38
|
/** Whether an existing rail can type back into this session (see module doc). */
|
|
39
39
|
reply: ReplyRail;
|
|
40
|
+
/** The initiating device, resolved at query time from ssh.clientIp against the
|
|
41
|
+
* device registry (ssh transport only). Answers "who launched this and from
|
|
42
|
+
* where" without scraping ps/who/tailscale. */
|
|
43
|
+
origin?: {
|
|
44
|
+
device: string;
|
|
45
|
+
user?: string;
|
|
46
|
+
};
|
|
40
47
|
}
|
|
41
48
|
/** Env vars that carry provenance. Kept small so the macOS `ps` scan stays cheap. */
|
|
42
49
|
export declare const PROVENANCE_ENV_KEYS: readonly ["SSH_CONNECTION", "SSH_TTY", "TMUX", "TMUX_PANE", "TERM_PROGRAM", "STY", "ITERM_SESSION_ID"];
|
|
@@ -13,6 +13,7 @@ import { cleanSessionPrompt, extractSessionTopic } from './prompt.js';
|
|
|
13
13
|
import { renderMarkdown } from '../markdown.js';
|
|
14
14
|
import { redactSecrets } from '../redact.js';
|
|
15
15
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from './digest.js';
|
|
16
|
+
import { extractTodoProgressFromEvents } from './state.js';
|
|
16
17
|
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
17
18
|
/**
|
|
18
19
|
* Return absPath relative to cwd; fall back to ~/… then absolute.
|
|
@@ -528,7 +529,7 @@ export function renderSummary(events, cwd) {
|
|
|
528
529
|
// Commands with timestamps
|
|
529
530
|
const cmdList = [];
|
|
530
531
|
// Plan items
|
|
531
|
-
const todoItems = [];
|
|
532
|
+
const todoItems = extractTodoProgressFromEvents(events)?.items.map(item => item.content) ?? [];
|
|
532
533
|
let exitPlanContent = null;
|
|
533
534
|
let planFilePath = null;
|
|
534
535
|
// Subagent spawns
|
|
@@ -569,19 +570,6 @@ export function renderSummary(events, cwd) {
|
|
|
569
570
|
recentActivity.push({ kind: 'cmd', label: cmd, ts });
|
|
570
571
|
}
|
|
571
572
|
}
|
|
572
|
-
// Plan items: TodoWrite items + TaskCreate descriptions (project's task tracker)
|
|
573
|
-
if (tool === 'TodoWrite' && Array.isArray(args.todos)) {
|
|
574
|
-
for (const item of args.todos) {
|
|
575
|
-
const text = item.content || item.text || String(item);
|
|
576
|
-
if (text && !todoItems.includes(text))
|
|
577
|
-
todoItems.push(text);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
if (tool === 'TaskCreate' && (args.description || args.prompt)) {
|
|
581
|
-
const text = String(args.description || args.prompt || '').slice(0, 140);
|
|
582
|
-
if (text && !todoItems.includes(text))
|
|
583
|
-
todoItems.push(text);
|
|
584
|
-
}
|
|
585
573
|
if (tool === 'ExitPlanMode') {
|
|
586
574
|
exitPlanContent = args.result || args.plan || args.content || null;
|
|
587
575
|
}
|
|
@@ -796,6 +784,9 @@ export function parseRoleList(raw, flag) {
|
|
|
796
784
|
return parts;
|
|
797
785
|
}
|
|
798
786
|
function roleOfEvent(e) {
|
|
787
|
+
// Synthetic scaffolding still maps to 'user' here so `--exclude user` drops it
|
|
788
|
+
// like any other user-role event (pre-flag behavior). `--include user`'s
|
|
789
|
+
// "genuine intent only" carve-out lives in applyRoleFilter, not here.
|
|
799
790
|
if (e.type === 'message' && e.role === 'user')
|
|
800
791
|
return 'user';
|
|
801
792
|
if (e.type === 'message' && e.role === 'assistant')
|
|
@@ -817,7 +808,14 @@ function applyRoleFilter(events, opts) {
|
|
|
817
808
|
const set = new Set(opts.include);
|
|
818
809
|
return events.filter(e => {
|
|
819
810
|
const role = roleOfEvent(e);
|
|
820
|
-
|
|
811
|
+
if (role === null)
|
|
812
|
+
return false;
|
|
813
|
+
// `--include user` means genuine user intent, so drop harness-injected
|
|
814
|
+
// `_synthetic` scaffolding (bash-input, system-reminder) even though it
|
|
815
|
+
// carries role=user. `--exclude user` still drops it via roleOfEvent.
|
|
816
|
+
if (role === 'user' && e._synthetic)
|
|
817
|
+
return false;
|
|
818
|
+
return set.has(role);
|
|
821
819
|
});
|
|
822
820
|
}
|
|
823
821
|
if (opts.exclude && opts.exclude.length > 0) {
|
|
@@ -845,7 +843,11 @@ function applyTurnSlice(events, opts) {
|
|
|
845
843
|
if (!Number.isFinite(n) || n <= 0) {
|
|
846
844
|
throw new Error(`Turn count must be a positive integer, got ${n}`);
|
|
847
845
|
}
|
|
848
|
-
|
|
846
|
+
// A turn starts at a GENUINE user message — harness-injected `_synthetic`
|
|
847
|
+
// scaffolding (`<bash-input>`/`<bash-stdout>`, etc.) does not start a turn,
|
|
848
|
+
// so `--first N` / `--last N` count real asks, not the jump command that
|
|
849
|
+
// opened the session and its shell output.
|
|
850
|
+
const isTurnStart = (e) => e.type === 'message' && e.role === 'user' && !e._synthetic;
|
|
849
851
|
const turnStartIdx = [];
|
|
850
852
|
for (let i = 0; i < events.length; i++)
|
|
851
853
|
if (isTurnStart(events[i]))
|
|
@@ -126,6 +126,11 @@ export interface StateContext {
|
|
|
126
126
|
* list, so a session with no plan carries no `todos` field.
|
|
127
127
|
*/
|
|
128
128
|
export declare function extractTodoProgress(args?: Record<string, any>): TodoProgress | undefined;
|
|
129
|
+
/** Fold snapshot checklist tools and Claude TaskCreate/TaskUpdate event logs. */
|
|
130
|
+
export declare function extractTodoProgressFromEvents(events: SessionEvent[]): TodoProgress | undefined;
|
|
131
|
+
/** Derive a recency-ordered, de-duplicated list of directories touched by tools. */
|
|
132
|
+
export declare function extractRecentDirectoriesTouched(events: SessionEvent[], cwd?: string): string[] | undefined;
|
|
133
|
+
export declare const WORKTREE_RE: RegExp;
|
|
129
134
|
/** Detect a worktree from the session cwd, per the `.agents/worktrees/<slug>/` convention. */
|
|
130
135
|
export declare function detectWorktree(cwd?: string, branch?: string): DetectedWorktree | undefined;
|
|
131
136
|
/** Detect a tracker ticket from free text (prompt/topic) then a branch name. */
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* markers. Codex has no such tools, so it falls back to last-role + question
|
|
15
15
|
* shape + mtime — same function, driven off the normalized events.
|
|
16
16
|
*/
|
|
17
|
+
import * as path from 'path';
|
|
17
18
|
import { summarizeToolUse } from './parse.js';
|
|
18
19
|
/**
|
|
19
20
|
* Detect per-session rate-limit / usage-limit signals in assistant or error
|
|
@@ -47,9 +48,9 @@ const PROSE_QUESTION_FRESH_MS = 30 * 60_000;
|
|
|
47
48
|
/** Claude tool names that structurally mean "the agent handed control back to you". */
|
|
48
49
|
const PLAN_TOOL = 'ExitPlanMode';
|
|
49
50
|
const ASK_TOOL = 'AskUserQuestion';
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
51
|
+
const SNAPSHOT_TODO_TOOLS = new Set(['TodoWrite', 'todo_write', 'update_plan']);
|
|
52
|
+
const TASK_CREATE_TOOL = 'TaskCreate';
|
|
53
|
+
const TASK_UPDATE_TOOL = 'TaskUpdate';
|
|
53
54
|
/**
|
|
54
55
|
* Derive live plan progress from a checklist tool call's args. Accepts both
|
|
55
56
|
* Claude's `TodoWrite` (`todos: [{content,status,activeForm}]`) and Codex's
|
|
@@ -58,10 +59,11 @@ const CODEX_PLAN_TOOL = 'update_plan';
|
|
|
58
59
|
* list, so a session with no plan carries no `todos` field.
|
|
59
60
|
*/
|
|
60
61
|
export function extractTodoProgress(args) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const input = args?.input && typeof args.input === 'object' ? args.input : args;
|
|
63
|
+
const raw = Array.isArray(input?.todos)
|
|
64
|
+
? input.todos
|
|
65
|
+
: Array.isArray(input?.plan)
|
|
66
|
+
? input.plan
|
|
65
67
|
: undefined;
|
|
66
68
|
if (!Array.isArray(raw) || raw.length === 0)
|
|
67
69
|
return undefined;
|
|
@@ -78,7 +80,8 @@ export function extractTodoProgress(args) {
|
|
|
78
80
|
if (!content)
|
|
79
81
|
continue;
|
|
80
82
|
const status = t?.status === 'completed' || t?.status === 'in_progress' ? t.status : 'pending';
|
|
81
|
-
|
|
83
|
+
const description = typeof t?.description === 'string' && t.description ? t.description : undefined;
|
|
84
|
+
items.push({ content, status, ...(description ? { description } : {}), ...(activeForm ? { activeForm } : {}) });
|
|
82
85
|
}
|
|
83
86
|
if (items.length === 0)
|
|
84
87
|
return undefined;
|
|
@@ -91,6 +94,86 @@ export function extractTodoProgress(args) {
|
|
|
91
94
|
activeForm: inProgress ? inProgress.activeForm ?? inProgress.content : undefined,
|
|
92
95
|
};
|
|
93
96
|
}
|
|
97
|
+
/** Fold snapshot checklist tools and Claude TaskCreate/TaskUpdate event logs. */
|
|
98
|
+
export function extractTodoProgressFromEvents(events) {
|
|
99
|
+
let items = [];
|
|
100
|
+
let nextTaskId = 1;
|
|
101
|
+
let sawChecklist = false;
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
if (event.type !== 'tool_use')
|
|
104
|
+
continue;
|
|
105
|
+
const args = event.args ?? {};
|
|
106
|
+
if (SNAPSHOT_TODO_TOOLS.has(event.tool ?? '')) {
|
|
107
|
+
const input = args.input && typeof args.input === 'object' ? args.input : args;
|
|
108
|
+
const raw = Array.isArray(input.todos) ? input.todos : Array.isArray(input.plan) ? input.plan : undefined;
|
|
109
|
+
if (raw) {
|
|
110
|
+
items = raw.map((item) => ({ ...item }));
|
|
111
|
+
sawChecklist = true;
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (event.tool === TASK_CREATE_TOOL) {
|
|
116
|
+
const content = args.subject || args.description;
|
|
117
|
+
if (typeof content !== 'string' || !content.trim())
|
|
118
|
+
continue;
|
|
119
|
+
items.push({
|
|
120
|
+
taskId: String(nextTaskId++),
|
|
121
|
+
content: content.trim(),
|
|
122
|
+
description: typeof args.description === 'string' ? args.description : undefined,
|
|
123
|
+
activeForm: typeof args.activeForm === 'string' ? args.activeForm : undefined,
|
|
124
|
+
status: 'pending',
|
|
125
|
+
});
|
|
126
|
+
sawChecklist = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (event.tool === TASK_UPDATE_TOOL) {
|
|
130
|
+
const taskId = String(args.taskId ?? args.task_id ?? '');
|
|
131
|
+
const index = items.findIndex(item => String(item.taskId ?? '') === taskId);
|
|
132
|
+
if (index < 0)
|
|
133
|
+
continue;
|
|
134
|
+
if (args.status === 'deleted') {
|
|
135
|
+
items.splice(index, 1);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const prior = items[index];
|
|
139
|
+
items[index] = {
|
|
140
|
+
...prior,
|
|
141
|
+
...(typeof args.subject === 'string' ? { content: args.subject } : {}),
|
|
142
|
+
...(typeof args.description === 'string' ? { description: args.description } : {}),
|
|
143
|
+
...(typeof args.activeForm === 'string' ? { activeForm: args.activeForm } : {}),
|
|
144
|
+
...(typeof args.status === 'string' ? { status: args.status } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return sawChecklist ? extractTodoProgress({ todos: items }) : undefined;
|
|
149
|
+
}
|
|
150
|
+
/** Derive a recency-ordered, de-duplicated list of directories touched by tools. */
|
|
151
|
+
export function extractRecentDirectoriesTouched(events, cwd) {
|
|
152
|
+
const dirs = [];
|
|
153
|
+
const add = (value, file = false) => {
|
|
154
|
+
if (typeof value !== 'string' || !value.trim())
|
|
155
|
+
return;
|
|
156
|
+
const resolved = path.isAbsolute(value) ? value : path.resolve(cwd || process.cwd(), value);
|
|
157
|
+
const dir = file ? path.dirname(resolved) : resolved;
|
|
158
|
+
const old = dirs.indexOf(dir);
|
|
159
|
+
if (old >= 0)
|
|
160
|
+
dirs.splice(old, 1);
|
|
161
|
+
dirs.push(dir);
|
|
162
|
+
};
|
|
163
|
+
for (const event of events) {
|
|
164
|
+
if (event.type !== 'tool_use')
|
|
165
|
+
continue;
|
|
166
|
+
const tool = event.tool ?? '';
|
|
167
|
+
const args = event.args ?? {};
|
|
168
|
+
if (['Edit', 'Write', 'edit_file', 'write_file', 'create_file', 'edit', 'write'].includes(tool)) {
|
|
169
|
+
add(args.file_path ?? args.filePath ?? args.path ?? event.path, true);
|
|
170
|
+
}
|
|
171
|
+
else if (['Bash', 'exec_command', 'run_shell_command', 'shell', 'Execute'].includes(tool)) {
|
|
172
|
+
add(args.cwd ?? args.Cwd ?? args.workdir ?? args.working_directory ?? cwd);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return dirs.length ? dirs.slice(-10) : undefined;
|
|
176
|
+
}
|
|
94
177
|
/** Trailing '?' or a leading interrogative — a question aimed at the user. */
|
|
95
178
|
const QUESTION_TRAILING = /\?["'”)\]]?\s*$/;
|
|
96
179
|
const QUESTION_PHRASE = /\b(shall i|should i|do you want|would you like|which (?:one|option|approach|of)|can you (?:confirm|clarify)|please (?:confirm|clarify|advise)|let me know|are you (?:ok|okay|sure)|proceed\?)\b/i;
|
|
@@ -105,7 +188,7 @@ const TICKET_BRANCH_RE = /(?:^|[/_-])([a-z]{2,6})-(\d{2,6})(?=[/_-]|$)/;
|
|
|
105
188
|
/** Keys that look like tickets but aren't — avoid false positives from branches. */
|
|
106
189
|
const TICKET_DENYLIST = new Set(['UTF', 'SHA', 'ISO', 'RFC', 'IPV', 'X86', 'ARM', 'MP', 'H']);
|
|
107
190
|
const PR_URL_RE = /https:\/\/github\.com\/[^\s"'()<>]+\/pull\/(\d+)/;
|
|
108
|
-
const WORKTREE_RE = /\/\.agents\/worktrees\/([^/]+)/;
|
|
191
|
+
export const WORKTREE_RE = /\/\.agents\/worktrees\/([^/]+)/;
|
|
109
192
|
/** gh invocations that create/open a PR. */
|
|
110
193
|
const GH_PR_CREATE_RE = /\bgh\s+pr\s+(?:create|new)\b/;
|
|
111
194
|
/** gh invocation that opens an issue — the created number is read from its result. */
|
|
@@ -325,11 +408,10 @@ export function inferActivity(events, ctx = {}) {
|
|
|
325
408
|
.slice(-3)
|
|
326
409
|
.map(e => oneLine(e.content ?? ''))
|
|
327
410
|
.filter(Boolean);
|
|
328
|
-
// Live plan progress: the
|
|
411
|
+
// Live plan progress: snapshot checklist tools or the folded Task* event log. Attached
|
|
329
412
|
// to `base` so every return path below carries it — a working, waiting, or idle
|
|
330
413
|
// session all keep showing how far the plan got.
|
|
331
|
-
const
|
|
332
|
-
const todos = lastTodo ? extractTodoProgress(lastTodo.args) : undefined;
|
|
414
|
+
const todos = extractTodoProgressFromEvents(meaningful);
|
|
333
415
|
const base = {
|
|
334
416
|
activity: 'idle',
|
|
335
417
|
lastRole: lastMsg?.role,
|
|
@@ -15,8 +15,8 @@ function resolveR2Config() {
|
|
|
15
15
|
// The daemon monitor loop is headless by construction (no TTY, ever), so
|
|
16
16
|
// isHeadlessSecretsContext() is true here and this resolves broker-only — a
|
|
17
17
|
// broker miss can never pop an unattended Touch ID sheet on the user's screen
|
|
18
|
-
// (same rationale
|
|
19
|
-
// predicate rather than a literal keeps it consistent with the other callers
|
|
18
|
+
// (the same broker-only-when-headless rationale the secrets readers use). Using
|
|
19
|
+
// the shared predicate rather than a literal keeps it consistent with the other callers
|
|
20
20
|
// and lets any interactive caller of loadR2Config still prompt.
|
|
21
21
|
const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'sessions-sync', agentOnly: isHeadlessSecretsContext() });
|
|
22
22
|
const accountId = env.R2_ACCOUNT_ID?.trim();
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'antigravity' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid';
|
|
11
11
|
/** All agents with session discovery support, in display order. */
|
|
12
12
|
export declare const SESSION_AGENTS: SessionAgentId[];
|
|
13
|
+
/**
|
|
14
|
+
* True when `agent` stores session data `agents sessions` can discover (a member
|
|
15
|
+
* of {@link SESSION_AGENTS}). The single predicate every session-index writer
|
|
16
|
+
* gates on, so "is this a trackable agent?" is decided in exactly one place.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isSessionTrackedAgent(agent: string): agent is SessionAgentId;
|
|
13
19
|
/** A single normalized event within a session (message, tool call, thinking, etc.). */
|
|
14
20
|
export interface SessionEvent {
|
|
15
21
|
type: 'message' | 'tool_use' | 'tool_result' | 'thinking' | 'error' | 'init' | 'result' | 'usage' | 'attachment';
|
|
@@ -25,6 +31,16 @@ export interface SessionEvent {
|
|
|
25
31
|
output?: string;
|
|
26
32
|
/** Internal: marks tool_use events from local commands */
|
|
27
33
|
_local?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Internal: marks a `role=user` message that is harness-injected scaffolding
|
|
36
|
+
* (Claude `<bash-input>`/`<bash-stdout>` from `!`-prefix runs, `<system-reminder>`,
|
|
37
|
+
* `<task-notification>`, `<command-*>` wrappers, `[Request interrupted]`, skill
|
|
38
|
+
* bodies, hook feedback) rather than a genuine user turn. Set centrally in
|
|
39
|
+
* `parseSession` via `isSyntheticUserMessage`. Such events are excluded from
|
|
40
|
+
* `--include user` and are not counted as turn starts by `--first`/`--last`,
|
|
41
|
+
* but stay in the default full stream so `--markdown` keeps full fidelity.
|
|
42
|
+
*/
|
|
43
|
+
_synthetic?: boolean;
|
|
28
44
|
model?: string;
|
|
29
45
|
inputTokens?: number;
|
|
30
46
|
outputTokens?: number;
|
|
@@ -41,10 +57,12 @@ export interface SessionAttachment {
|
|
|
41
57
|
mediaType: string;
|
|
42
58
|
sizeBytes?: number;
|
|
43
59
|
}
|
|
44
|
-
/** One checklist item
|
|
60
|
+
/** One normalized checklist/task item emitted by any transcript harness. */
|
|
45
61
|
export interface TodoItem {
|
|
46
62
|
content: string;
|
|
47
63
|
status: 'pending' | 'in_progress' | 'completed';
|
|
64
|
+
/** Optional longer explanation supplied by task-based harnesses. */
|
|
65
|
+
description?: string;
|
|
48
66
|
/** Present-continuous label shown while this item is the active step. */
|
|
49
67
|
activeForm?: string;
|
|
50
68
|
}
|
|
@@ -152,6 +170,12 @@ export interface SessionMeta {
|
|
|
152
170
|
* instead of re-parsing the transcript. Absent when the session wrote no list.
|
|
153
171
|
*/
|
|
154
172
|
todos?: TodoProgress;
|
|
173
|
+
/** Most-recent unique directories changed or used as a shell working directory. */
|
|
174
|
+
recentDirectoriesTouched?: string[];
|
|
175
|
+
/** Linear project containing ticketId, resolved lazily and cached in SQLite. */
|
|
176
|
+
linearProject?: string;
|
|
177
|
+
/** Browser URL for linearProject. */
|
|
178
|
+
linearProjectUrl?: string;
|
|
155
179
|
/**
|
|
156
180
|
* True when the session was spawned programmatically (SDK entrypoint) rather
|
|
157
181
|
* than by a human at the Claude CLI. Captured at scan time from the JSONL
|
|
@@ -8,3 +8,11 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** All agents with session discovery support, in display order. */
|
|
10
10
|
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'antigravity', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid'];
|
|
11
|
+
/**
|
|
12
|
+
* True when `agent` stores session data `agents sessions` can discover (a member
|
|
13
|
+
* of {@link SESSION_AGENTS}). The single predicate every session-index writer
|
|
14
|
+
* gates on, so "is this a trackable agent?" is decided in exactly one place.
|
|
15
|
+
*/
|
|
16
|
+
export function isSessionTrackedAgent(agent) {
|
|
17
|
+
return SESSION_AGENTS.includes(agent);
|
|
18
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device affinity for `--device auto` / `--host auto`.
|
|
3
|
+
*
|
|
4
|
+
* Picks a host from 14d session usage (launches by machine), weighted sample
|
|
5
|
+
* among online devices. Most-used has highest probability — not a hard lock.
|
|
6
|
+
* Account selection is separate (`--strategy balanced` / rotate.ts).
|
|
7
|
+
*/
|
|
8
|
+
import { type AffinityRow } from './session/db.js';
|
|
9
|
+
/** Peakiness of usage weights; >1 amplifies the most-used option. */
|
|
10
|
+
export declare const DEFAULT_AFFINITY_ALPHA = 1.3;
|
|
11
|
+
export interface WeightedCandidate {
|
|
12
|
+
key: string;
|
|
13
|
+
launches: number;
|
|
14
|
+
weight: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Convert affinity rows into positive weights: launches^α (floor 1 for any
|
|
18
|
+
* row with launches > 0 so a single-use host still participates).
|
|
19
|
+
*/
|
|
20
|
+
export declare function affinityWeights(rows: AffinityRow[], alpha?: number): WeightedCandidate[];
|
|
21
|
+
/**
|
|
22
|
+
* Weighted random sample. Returns null when candidates is empty.
|
|
23
|
+
* Pure — inject `rng` for tests.
|
|
24
|
+
*/
|
|
25
|
+
export declare function sampleWeighted(candidates: WeightedCandidate[], rng?: () => number): string | null;
|
|
26
|
+
/** Online device names from the local registry (+ always include local). */
|
|
27
|
+
export declare function listOnlineDeviceNames(localName?: string): string[];
|
|
28
|
+
export interface DeviceAffinityOptions {
|
|
29
|
+
sinceDays?: number;
|
|
30
|
+
alpha?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Eligible hosts (normalized). Defaults to online devices + local.
|
|
33
|
+
* Empty after filter → fall back to local.
|
|
34
|
+
*/
|
|
35
|
+
eligibleHosts?: string[];
|
|
36
|
+
localMachine?: string;
|
|
37
|
+
/** Injected affinity (tests). When omitted, reads sessions.db. */
|
|
38
|
+
deviceAffinity?: AffinityRow[];
|
|
39
|
+
rng?: () => number;
|
|
40
|
+
project?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface DeviceAffinityPlan {
|
|
43
|
+
/** null means run locally (do not pass --host). */
|
|
44
|
+
host: string | null;
|
|
45
|
+
deviceCandidates: WeightedCandidate[];
|
|
46
|
+
pickedDeviceKey: string | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveDeviceAffinity(opts?: DeviceAffinityOptions): DeviceAffinityPlan;
|
|
52
|
+
/** True when a host flag value means affinity pick. */
|
|
53
|
+
export declare function isDeviceAuto(value: string | undefined | null): boolean;
|
|
54
|
+
export type DeviceAutoHostOptions = {
|
|
55
|
+
host?: string;
|
|
56
|
+
device?: string;
|
|
57
|
+
on?: string;
|
|
58
|
+
computer?: string;
|
|
59
|
+
/** @deprecated Hidden alias for `--device auto`. */
|
|
60
|
+
smart?: boolean;
|
|
61
|
+
balanced?: boolean;
|
|
62
|
+
strategy?: string;
|
|
63
|
+
};
|
|
64
|
+
export type DeviceAutoApplyResult = {
|
|
65
|
+
/** True when affinity ran (success or degrade-to-local). */
|
|
66
|
+
attempted: boolean;
|
|
67
|
+
/** True when deprecated `--smart` was seen. */
|
|
68
|
+
deprecationSmart: boolean;
|
|
69
|
+
/** Affinity threw: host slots cleared → local; message for stderr. */
|
|
70
|
+
skipped?: string;
|
|
71
|
+
/** Present when affinity resolved without error. */
|
|
72
|
+
banner?: {
|
|
73
|
+
hostLabel: string;
|
|
74
|
+
deviceHint: string;
|
|
75
|
+
acctNote: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Apply `--device auto` / `--host auto` (and deprecated `--smart`) onto run options.
|
|
80
|
+
* Mutates `options` in place. On affinity failure, clears auto slots (local) and
|
|
81
|
+
* returns `skipped` — never throws; callers must not hard-exit.
|
|
82
|
+
*/
|
|
83
|
+
export declare function applyDeviceAutoToOptions(options: DeviceAutoHostOptions, deps?: {
|
|
84
|
+
resolve?: () => DeviceAffinityPlan;
|
|
85
|
+
accountPickerRequested?: boolean;
|
|
86
|
+
}): DeviceAutoApplyResult;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device affinity for `--device auto` / `--host auto`.
|
|
3
|
+
*
|
|
4
|
+
* Picks a host from 14d session usage (launches by machine), weighted sample
|
|
5
|
+
* among online devices. Most-used has highest probability — not a hard lock.
|
|
6
|
+
* Account selection is separate (`--strategy balanced` / rotate.ts).
|
|
7
|
+
*/
|
|
8
|
+
import { queryAffinityRollup } from './session/db.js';
|
|
9
|
+
import { localMachineId } from './session/origin-machine.js';
|
|
10
|
+
import { loadDevicesSync } from './devices/registry.js';
|
|
11
|
+
import { normalizeHost } from './machine-id.js';
|
|
12
|
+
/** Peakiness of usage weights; >1 amplifies the most-used option. */
|
|
13
|
+
export const DEFAULT_AFFINITY_ALPHA = 1.3;
|
|
14
|
+
/**
|
|
15
|
+
* Convert affinity rows into positive weights: launches^α (floor 1 for any
|
|
16
|
+
* row with launches > 0 so a single-use host still participates).
|
|
17
|
+
*/
|
|
18
|
+
export function affinityWeights(rows, alpha = DEFAULT_AFFINITY_ALPHA) {
|
|
19
|
+
return rows
|
|
20
|
+
.filter((r) => r.launches > 0 && r.key && r.key !== '(unknown)')
|
|
21
|
+
.map((r) => ({
|
|
22
|
+
key: r.key,
|
|
23
|
+
launches: r.launches,
|
|
24
|
+
weight: Math.max(1, Math.pow(r.launches, alpha)),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Weighted random sample. Returns null when candidates is empty.
|
|
29
|
+
* Pure — inject `rng` for tests.
|
|
30
|
+
*/
|
|
31
|
+
export function sampleWeighted(candidates, rng = Math.random) {
|
|
32
|
+
if (candidates.length === 0)
|
|
33
|
+
return null;
|
|
34
|
+
const total = candidates.reduce((s, c) => s + c.weight, 0);
|
|
35
|
+
if (total <= 0)
|
|
36
|
+
return candidates[0].key;
|
|
37
|
+
let roll = rng() * total;
|
|
38
|
+
for (const c of candidates) {
|
|
39
|
+
roll -= c.weight;
|
|
40
|
+
if (roll <= 0)
|
|
41
|
+
return c.key;
|
|
42
|
+
}
|
|
43
|
+
return candidates[candidates.length - 1].key;
|
|
44
|
+
}
|
|
45
|
+
/** Online device names from the local registry (+ always include local). */
|
|
46
|
+
export function listOnlineDeviceNames(localName = localMachineId()) {
|
|
47
|
+
const names = new Set([normalizeHost(localName)]);
|
|
48
|
+
try {
|
|
49
|
+
const reg = loadDevicesSync();
|
|
50
|
+
for (const [name, d] of Object.entries(reg)) {
|
|
51
|
+
if (!d || typeof d !== 'object')
|
|
52
|
+
continue;
|
|
53
|
+
const online = d.tailscale?.online;
|
|
54
|
+
// No tailscale snapshot → treat as candidate (registry-only box).
|
|
55
|
+
if (online === false)
|
|
56
|
+
continue;
|
|
57
|
+
names.add(normalizeHost(name));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* registry missing — local only */
|
|
62
|
+
}
|
|
63
|
+
return [...names];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve host for `--device auto`. Does NOT pick harness or accounts.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveDeviceAffinity(opts = {}) {
|
|
69
|
+
const local = normalizeHost(opts.localMachine ?? localMachineId());
|
|
70
|
+
const alpha = opts.alpha ?? DEFAULT_AFFINITY_ALPHA;
|
|
71
|
+
const rng = opts.rng ?? Math.random;
|
|
72
|
+
const sinceDays = opts.sinceDays ?? 14;
|
|
73
|
+
const sinceMs = Date.now() - sinceDays * 24 * 60 * 60 * 1000;
|
|
74
|
+
const eligible = new Set((opts.eligibleHosts ?? listOnlineDeviceNames(local)).map(normalizeHost));
|
|
75
|
+
if (eligible.size === 0)
|
|
76
|
+
eligible.add(local);
|
|
77
|
+
const deviceRows = opts.deviceAffinity ??
|
|
78
|
+
queryAffinityRollup({
|
|
79
|
+
groupBy: 'machine',
|
|
80
|
+
sinceMs,
|
|
81
|
+
excludeTeamOrigin: true,
|
|
82
|
+
onlyCli: true,
|
|
83
|
+
project: opts.project,
|
|
84
|
+
});
|
|
85
|
+
// Hosts with history get launches^α; online hosts with no history explore at weight 1.
|
|
86
|
+
const launchesByHost = new Map();
|
|
87
|
+
for (const r of deviceRows) {
|
|
88
|
+
const k = normalizeHost(r.key);
|
|
89
|
+
if (!eligible.has(k) || k === '(unknown)')
|
|
90
|
+
continue;
|
|
91
|
+
launchesByHost.set(k, (launchesByHost.get(k) ?? 0) + r.launches);
|
|
92
|
+
}
|
|
93
|
+
let deviceWeights = [...eligible].map((key) => {
|
|
94
|
+
const launches = launchesByHost.get(key) ?? 0;
|
|
95
|
+
return {
|
|
96
|
+
key,
|
|
97
|
+
launches,
|
|
98
|
+
weight: launches > 0 ? Math.max(1, Math.pow(launches, alpha)) : 1,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
if (deviceWeights.length === 0) {
|
|
102
|
+
deviceWeights = [{ key: local, launches: 1, weight: 1 }];
|
|
103
|
+
}
|
|
104
|
+
const pickedDeviceKey = sampleWeighted(deviceWeights, rng);
|
|
105
|
+
const hostNorm = pickedDeviceKey ? normalizeHost(pickedDeviceKey) : local;
|
|
106
|
+
const host = hostNorm === local ? null : hostNorm;
|
|
107
|
+
return {
|
|
108
|
+
host,
|
|
109
|
+
deviceCandidates: deviceWeights,
|
|
110
|
+
pickedDeviceKey,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** True when a host flag value means affinity pick. */
|
|
114
|
+
export function isDeviceAuto(value) {
|
|
115
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'auto';
|
|
116
|
+
}
|
|
117
|
+
const HOST_SLOTS = ['host', 'device', 'on', 'computer'];
|
|
118
|
+
/**
|
|
119
|
+
* Apply `--device auto` / `--host auto` (and deprecated `--smart`) onto run options.
|
|
120
|
+
* Mutates `options` in place. On affinity failure, clears auto slots (local) and
|
|
121
|
+
* returns `skipped` — never throws; callers must not hard-exit.
|
|
122
|
+
*/
|
|
123
|
+
export function applyDeviceAutoToOptions(options, deps = {}) {
|
|
124
|
+
let deprecationSmart = false;
|
|
125
|
+
if (options.smart) {
|
|
126
|
+
const anyHost = HOST_SLOTS.some((k) => typeof options[k] === 'string' && options[k].trim() !== '');
|
|
127
|
+
if (!anyHost)
|
|
128
|
+
options.device = 'auto';
|
|
129
|
+
deprecationSmart = true;
|
|
130
|
+
}
|
|
131
|
+
const hasAuto = HOST_SLOTS.some((k) => isDeviceAuto(options[k]));
|
|
132
|
+
if (!hasAuto) {
|
|
133
|
+
return { attempted: false, deprecationSmart };
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const plan = (deps.resolve ?? (() => resolveDeviceAffinity({})))();
|
|
137
|
+
const concrete = plan.host; // null = local
|
|
138
|
+
for (const k of HOST_SLOTS) {
|
|
139
|
+
if (isDeviceAuto(options[k])) {
|
|
140
|
+
options[k] = concrete ?? undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const accountPickerRequested = deps.accountPickerRequested ?? false;
|
|
144
|
+
if (!accountPickerRequested && !options.strategy && !options.balanced) {
|
|
145
|
+
options.balanced = true;
|
|
146
|
+
}
|
|
147
|
+
const hostLabel = concrete ?? 'local';
|
|
148
|
+
const deviceHint = plan.deviceCandidates
|
|
149
|
+
.slice(0, 4)
|
|
150
|
+
.map((c) => `${c.key}:${c.launches}`)
|
|
151
|
+
.join(', ');
|
|
152
|
+
const acctNote = accountPickerRequested ? 'accounts=picker' : 'accounts=balanced';
|
|
153
|
+
return {
|
|
154
|
+
attempted: true,
|
|
155
|
+
deprecationSmart,
|
|
156
|
+
banner: { hostLabel, deviceHint, acctNote },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// Graceful degrade: clear auto slots so the run continues locally.
|
|
161
|
+
for (const k of HOST_SLOTS) {
|
|
162
|
+
if (isDeviceAuto(options[k])) {
|
|
163
|
+
options[k] = undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
attempted: true,
|
|
168
|
+
deprecationSmart,
|
|
169
|
+
skipped: err.message,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -121,9 +121,13 @@ export declare function getSystemRoutinesDir(): string;
|
|
|
121
121
|
* Path to a project-scoped routines directory (`<project>/.agents/routines/`),
|
|
122
122
|
* or null when no project `.agents/` is found by walking up from cwd.
|
|
123
123
|
*
|
|
124
|
-
* Project routines participate in `list`/`view
|
|
125
|
-
*
|
|
126
|
-
* routines)
|
|
124
|
+
* Project routines participate in `list`/`view` for inspection always. Daemon
|
|
125
|
+
* firing requires an explicit opt-in via `agents routines enable-project`
|
|
126
|
+
* (writes `meta.routines.projects`); after opt-in, `agents routines sync`
|
|
127
|
+
* materialises them into the user layer with `source:` provenance so the
|
|
128
|
+
* daemon (which loads user + system only) can fire them safely. A project's
|
|
129
|
+
* own `agents.yaml` `routines.enable: true` is a documentation signal only —
|
|
130
|
+
* it does not auto-enable firing. See `lib/routines-project.ts`.
|
|
127
131
|
*/
|
|
128
132
|
export declare function getProjectRoutinesDir(cwd?: string): string | null;
|
|
129
133
|
/** Path to routine execution logs (~/.agents/.history/runs/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -319,9 +319,13 @@ export function getSystemRoutinesDir() { return SYSTEM_ROUTINES_DIR; }
|
|
|
319
319
|
* Path to a project-scoped routines directory (`<project>/.agents/routines/`),
|
|
320
320
|
* or null when no project `.agents/` is found by walking up from cwd.
|
|
321
321
|
*
|
|
322
|
-
* Project routines participate in `list`/`view
|
|
323
|
-
*
|
|
324
|
-
* routines)
|
|
322
|
+
* Project routines participate in `list`/`view` for inspection always. Daemon
|
|
323
|
+
* firing requires an explicit opt-in via `agents routines enable-project`
|
|
324
|
+
* (writes `meta.routines.projects`); after opt-in, `agents routines sync`
|
|
325
|
+
* materialises them into the user layer with `source:` provenance so the
|
|
326
|
+
* daemon (which loads user + system only) can fire them safely. A project's
|
|
327
|
+
* own `agents.yaml` `routines.enable: true` is a documentation signal only —
|
|
328
|
+
* it does not auto-enable firing. See `lib/routines-project.ts`.
|
|
325
329
|
*/
|
|
326
330
|
export function getProjectRoutinesDir(cwd = process.cwd()) {
|
|
327
331
|
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
@@ -383,10 +387,21 @@ export function getTeamsDir() { return TEAMS_DIR; }
|
|
|
383
387
|
export function getTeamsAgentsDir() { return TEAMS_AGENTS_DIR; }
|
|
384
388
|
/** Path to the team registry — list of named teams with timestamps. Durable runtime, per-machine. */
|
|
385
389
|
export function getTeamsRegistryPath() { return path.join(HISTORY_DIR, 'teams', 'registry.json'); }
|
|
390
|
+
/**
|
|
391
|
+
* The devices dir (registry + ignore-list live here). Read at CALL time so a
|
|
392
|
+
* test can redirect it to a temp dir via AGENTS_DEVICES_DIR without racing the
|
|
393
|
+
* module-load capture of HISTORY_DIR — mirrors the AGENTS_EVENTS_PATH /
|
|
394
|
+
* AGENTS_SECRETS_AGENT_DIR test-isolation escape hatches. Never set in
|
|
395
|
+
* production code; it exists so the vitest fork's device-registry writes can
|
|
396
|
+
* never reach the user's real ~/.agents/.history/devices (RUSH-2042).
|
|
397
|
+
*/
|
|
398
|
+
function getDevicesDir() {
|
|
399
|
+
return process.env.AGENTS_DEVICES_DIR ?? path.join(HISTORY_DIR, 'devices');
|
|
400
|
+
}
|
|
386
401
|
/** Path to the device registry — SSH device profiles with platform/auth metadata. Durable runtime, per-machine (host list + addresses are NOT pulled by `agents repo push`). */
|
|
387
|
-
export function getDevicesRegistryPath() { return path.join(
|
|
402
|
+
export function getDevicesRegistryPath() { return path.join(getDevicesDir(), 'registry.json'); }
|
|
388
403
|
/** Path to the device ignore-list — tailscale node names the user dismissed, so auto-discovery never re-suggests them. Per-machine, same dir as the registry. */
|
|
389
|
-
export function getDevicesIgnoredPath() { return path.join(
|
|
404
|
+
export function getDevicesIgnoredPath() { return path.join(getDevicesDir(), 'ignored.json'); }
|
|
390
405
|
/** Dir of "pending device" sentinels (~/.agents/.cache/state/devices-pending/) — one empty-ish file per newly-discovered, not-yet-approved tailnet node. Written by the daemon probe, read by the menu-bar helper (mirrors the attention sentinel dir). */
|
|
391
406
|
export function getDevicesPendingDir() { return path.join(RUNTIME_STATE_DIR, 'devices-pending'); }
|
|
392
407
|
/** Path to cloud dispatch cache (~/.agents/.cache/cloud/). */
|