@phnx-labs/agents-cli 1.20.72 → 1.20.74

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.
Files changed (234) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +21 -5
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/export.d.ts +11 -0
  16. package/dist/commands/export.js +215 -0
  17. package/dist/commands/feed.d.ts +13 -0
  18. package/dist/commands/feed.js +130 -29
  19. package/dist/commands/hq.d.ts +2 -0
  20. package/dist/commands/hq.js +58 -0
  21. package/dist/commands/import.js +51 -3
  22. package/dist/commands/lease.d.ts +19 -0
  23. package/dist/commands/lease.js +138 -11
  24. package/dist/commands/login.d.ts +2 -0
  25. package/dist/commands/login.js +123 -0
  26. package/dist/commands/logs.js +1 -1
  27. package/dist/commands/mine.d.ts +27 -0
  28. package/dist/commands/mine.js +207 -0
  29. package/dist/commands/monitors.js +79 -49
  30. package/dist/commands/plugins.js +1 -0
  31. package/dist/commands/profiles.js +158 -0
  32. package/dist/commands/repo.js +2 -60
  33. package/dist/commands/resources.d.ts +5 -0
  34. package/dist/commands/resources.js +95 -0
  35. package/dist/commands/routines.d.ts +2 -0
  36. package/dist/commands/routines.js +139 -37
  37. package/dist/commands/secrets.js +234 -47
  38. package/dist/commands/send.d.ts +15 -0
  39. package/dist/commands/send.js +62 -0
  40. package/dist/commands/sessions-tail.d.ts +1 -0
  41. package/dist/commands/sessions-tail.js +20 -9
  42. package/dist/commands/sessions.js +18 -3
  43. package/dist/commands/setup-mine.d.ts +18 -0
  44. package/dist/commands/setup-mine.js +106 -0
  45. package/dist/commands/setup-share.js +2 -2
  46. package/dist/commands/setup.js +9 -2
  47. package/dist/commands/share.d.ts +8 -1
  48. package/dist/commands/share.js +122 -37
  49. package/dist/commands/ssh.d.ts +9 -0
  50. package/dist/commands/ssh.js +196 -26
  51. package/dist/commands/sync.js +1 -1
  52. package/dist/commands/teams.js +11 -2
  53. package/dist/commands/versions.js +36 -5
  54. package/dist/commands/view.js +75 -27
  55. package/dist/index.js +80 -10
  56. package/dist/lib/activity.d.ts +124 -0
  57. package/dist/lib/activity.js +542 -0
  58. package/dist/lib/agents.d.ts +35 -0
  59. package/dist/lib/agents.js +79 -6
  60. package/dist/lib/artifact-actions.d.ts +1 -1
  61. package/dist/lib/artifact-actions.js +1 -1
  62. package/dist/lib/ask-classifier.d.ts +2 -1
  63. package/dist/lib/ask-classifier.js +3 -3
  64. package/dist/lib/auth-health.d.ts +32 -4
  65. package/dist/lib/auth-health.js +40 -1
  66. package/dist/lib/auto-pull.js +8 -1
  67. package/dist/lib/brand.d.ts +40 -0
  68. package/dist/lib/brand.js +122 -0
  69. package/dist/lib/browser/drivers/ssh.js +4 -7
  70. package/dist/lib/browser/service.js +6 -8
  71. package/dist/lib/channels/providers/index.d.ts +2 -0
  72. package/dist/lib/channels/providers/index.js +20 -0
  73. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  74. package/dist/lib/channels/providers/mailbox.js +26 -0
  75. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  76. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  77. package/dist/lib/channels/providers/rush.d.ts +6 -0
  78. package/dist/lib/channels/providers/rush.js +55 -0
  79. package/dist/lib/channels/registry.d.ts +42 -0
  80. package/dist/lib/channels/registry.js +20 -0
  81. package/dist/lib/channels/resolve.d.ts +11 -0
  82. package/dist/lib/channels/resolve.js +12 -0
  83. package/dist/lib/cli-resources.d.ts +18 -0
  84. package/dist/lib/cli-resources.js +53 -4
  85. package/dist/lib/cloud/codex.js +6 -3
  86. package/dist/lib/cloud/factory.d.ts +1 -0
  87. package/dist/lib/cloud/factory.js +10 -4
  88. package/dist/lib/cloud/rush.d.ts +3 -1
  89. package/dist/lib/cloud/rush.js +6 -1
  90. package/dist/lib/cloud/types.d.ts +2 -0
  91. package/dist/lib/codex-home.d.ts +35 -0
  92. package/dist/lib/codex-home.js +136 -0
  93. package/dist/lib/config-transfer.d.ts +16 -0
  94. package/dist/lib/config-transfer.js +56 -0
  95. package/dist/lib/crabbox/cli.d.ts +44 -0
  96. package/dist/lib/crabbox/cli.js +120 -20
  97. package/dist/lib/crabbox/lease.d.ts +52 -1
  98. package/dist/lib/crabbox/lease.js +117 -16
  99. package/dist/lib/crabbox/progress.d.ts +31 -0
  100. package/dist/lib/crabbox/progress.js +76 -0
  101. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  102. package/dist/lib/crabbox/runtimes.js +33 -8
  103. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  104. package/dist/lib/crabbox/setup-copy.js +127 -0
  105. package/dist/lib/daemon.d.ts +5 -5
  106. package/dist/lib/daemon.js +9 -9
  107. package/dist/lib/drive-sync.js +6 -3
  108. package/dist/lib/event-stream.d.ts +36 -0
  109. package/dist/lib/event-stream.js +68 -0
  110. package/dist/lib/events.d.ts +1 -1
  111. package/dist/lib/exec.d.ts +11 -0
  112. package/dist/lib/exec.js +23 -2
  113. package/dist/lib/export.d.ts +72 -0
  114. package/dist/lib/export.js +269 -0
  115. package/dist/lib/feed-policy.d.ts +1 -1
  116. package/dist/lib/feed-policy.js +14 -9
  117. package/dist/lib/feed-ranking.d.ts +32 -0
  118. package/dist/lib/feed-ranking.js +224 -0
  119. package/dist/lib/feed.d.ts +35 -2
  120. package/dist/lib/feed.js +66 -3
  121. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  122. package/dist/lib/fleet/auth-sync.js +92 -0
  123. package/dist/lib/fleet/remote-login.d.ts +187 -0
  124. package/dist/lib/fleet/remote-login.js +556 -0
  125. package/dist/lib/format.d.ts +30 -3
  126. package/dist/lib/format.js +34 -5
  127. package/dist/lib/fs-atomic.d.ts +7 -2
  128. package/dist/lib/fs-atomic.js +8 -12
  129. package/dist/lib/git.d.ts +16 -0
  130. package/dist/lib/git.js +79 -2
  131. package/dist/lib/heal.js +11 -3
  132. package/dist/lib/hosts/progress.d.ts +28 -10
  133. package/dist/lib/hosts/progress.js +79 -22
  134. package/dist/lib/hosts/remote-cmd.js +4 -0
  135. package/dist/lib/hq/floor.d.ts +87 -0
  136. package/dist/lib/hq/floor.js +226 -0
  137. package/dist/lib/import.d.ts +23 -0
  138. package/dist/lib/import.js +74 -2
  139. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  140. package/dist/lib/isolation-boundary-report.js +35 -0
  141. package/dist/lib/menubar/install-menubar.js +14 -20
  142. package/dist/lib/notify.d.ts +1 -0
  143. package/dist/lib/notify.js +3 -3
  144. package/dist/lib/open-url.d.ts +2 -0
  145. package/dist/lib/open-url.js +19 -0
  146. package/dist/lib/openclaw-keychain.d.ts +56 -0
  147. package/dist/lib/openclaw-keychain.js +236 -0
  148. package/dist/lib/overdue.js +10 -0
  149. package/dist/lib/permissions.d.ts +44 -1
  150. package/dist/lib/permissions.js +284 -7
  151. package/dist/lib/project-launch.d.ts +6 -12
  152. package/dist/lib/project-launch.js +13 -228
  153. package/dist/lib/project-resources.d.ts +7 -0
  154. package/dist/lib/project-resources.js +306 -0
  155. package/dist/lib/pty-client.d.ts +27 -0
  156. package/dist/lib/pty-client.js +136 -10
  157. package/dist/lib/refresh.js +14 -10
  158. package/dist/lib/resource-profiles.d.ts +26 -0
  159. package/dist/lib/resource-profiles.js +157 -0
  160. package/dist/lib/resources/permissions.js +7 -1
  161. package/dist/lib/resources/types.d.ts +1 -1
  162. package/dist/lib/resources.d.ts +1 -1
  163. package/dist/lib/resources.js +32 -3
  164. package/dist/lib/routines.d.ts +11 -0
  165. package/dist/lib/routines.js +56 -15
  166. package/dist/lib/runner.d.ts +1 -0
  167. package/dist/lib/runner.js +66 -5
  168. package/dist/lib/secrets/bundles.d.ts +16 -3
  169. package/dist/lib/secrets/bundles.js +206 -37
  170. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  171. package/dist/lib/secrets/icloud-import.js +9 -6
  172. package/dist/lib/secrets/mcp.js +1 -1
  173. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  174. package/dist/lib/secrets/vault-age-helper.js +34 -0
  175. package/dist/lib/secrets/vault.d.ts +49 -0
  176. package/dist/lib/secrets/vault.js +397 -0
  177. package/dist/lib/self-heal/checks/path.js +3 -1
  178. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  179. package/dist/lib/self-heal/checks/shims.js +19 -11
  180. package/dist/lib/session/cloud.d.ts +2 -2
  181. package/dist/lib/session/cloud.js +2 -2
  182. package/dist/lib/session/db.d.ts +4 -0
  183. package/dist/lib/session/db.js +44 -7
  184. package/dist/lib/session/discover.d.ts +2 -0
  185. package/dist/lib/session/discover.js +114 -3
  186. package/dist/lib/session/stream-render.d.ts +3 -0
  187. package/dist/lib/session/stream-render.js +130 -0
  188. package/dist/lib/session/types.d.ts +6 -0
  189. package/dist/lib/share/analytics.d.ts +13 -0
  190. package/dist/lib/share/analytics.js +45 -0
  191. package/dist/lib/share/config.d.ts +19 -5
  192. package/dist/lib/share/config.js +44 -8
  193. package/dist/lib/share/provision.d.ts +58 -6
  194. package/dist/lib/share/provision.js +97 -22
  195. package/dist/lib/share/publish.d.ts +36 -13
  196. package/dist/lib/share/publish.js +55 -8
  197. package/dist/lib/share/worker-template.js +83 -17
  198. package/dist/lib/shims.d.ts +35 -0
  199. package/dist/lib/shims.js +159 -13
  200. package/dist/lib/ssh-exec.d.ts +14 -0
  201. package/dist/lib/ssh-exec.js +57 -0
  202. package/dist/lib/staleness/detectors/hooks.js +25 -1
  203. package/dist/lib/staleness/detectors/permissions.js +66 -0
  204. package/dist/lib/staleness/detectors/workflows.js +20 -0
  205. package/dist/lib/staleness/writers/hooks.js +58 -4
  206. package/dist/lib/staleness/writers/permissions.js +2 -2
  207. package/dist/lib/staleness/writers/skills.js +1 -1
  208. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  209. package/dist/lib/staleness/writers/sources.js +68 -1
  210. package/dist/lib/star-nudge.d.ts +45 -0
  211. package/dist/lib/star-nudge.js +91 -0
  212. package/dist/lib/startup/command-registry.d.ts +7 -1
  213. package/dist/lib/startup/command-registry.js +19 -3
  214. package/dist/lib/state.d.ts +2 -0
  215. package/dist/lib/state.js +17 -4
  216. package/dist/lib/subagents-registry.d.ts +2 -0
  217. package/dist/lib/subagents-registry.js +3 -0
  218. package/dist/lib/teams/parsers.js +214 -6
  219. package/dist/lib/teams/supervisor.d.ts +7 -0
  220. package/dist/lib/teams/supervisor.js +2 -1
  221. package/dist/lib/template.d.ts +1 -1
  222. package/dist/lib/template.js +1 -1
  223. package/dist/lib/triggers/webhook.js +36 -3
  224. package/dist/lib/types.d.ts +84 -0
  225. package/dist/lib/uninstall.js +1 -45
  226. package/dist/lib/version.d.ts +40 -0
  227. package/dist/lib/version.js +94 -16
  228. package/dist/lib/versions.d.ts +24 -0
  229. package/dist/lib/versions.js +269 -78
  230. package/dist/lib/workflows.d.ts +2 -0
  231. package/dist/lib/workflows.js +88 -0
  232. package/package.json +2 -1
  233. package/dist/commands/daemon.d.ts +0 -10
  234. package/dist/commands/daemon.js +0 -121
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Activity log -- an append-only, per-session stream of agent-semantic events
3
+ * (plan created, PR opened, worktree created, sub-agent spawned, file edited).
4
+ *
5
+ * This is the counterpart to the feed block store: the block store is
6
+ * last-writer-wins STATE ("this agent is waiting on you"), while the activity
7
+ * log is an append-only EVENT stream ("this agent did X at T"). Together they
8
+ * let `agents feed` show both open decisions and a running activity lane
9
+ * without ever re-parsing session transcripts -- events are emitted at hook
10
+ * time by `11-activity-log.py` and read back by tailing the log.
11
+ *
12
+ * Layout: <activityDir>/<sessionId>.jsonl (one append-only file per session)
13
+ * Each line is one {@link ActivityEvent}. Files are keyed by session so a
14
+ * plain O_APPEND write is race-free (a session has a single writer process).
15
+ *
16
+ * Event tiers:
17
+ * - `milestone` -- recognizable deliverables, always surfaced individually.
18
+ * - `activity` -- routine work (file edits), collapsed to counts by readers.
19
+ */
20
+ import * as fs from 'fs';
21
+ import * as path from 'path';
22
+ import * as yaml from 'yaml';
23
+ import chalk from 'chalk';
24
+ import { relTime, truncate } from './format.js';
25
+ import { getActivityDir, getUserAgentsDir } from './state.js';
26
+ /** The set of milestone events, for tier classification and ordering. */
27
+ export const MILESTONE_EVENTS = [
28
+ 'plan.created',
29
+ 'pr.opened',
30
+ 'pr.merged',
31
+ 'worktree.created',
32
+ 'worktree.removed',
33
+ 'commit.created',
34
+ 'pushed',
35
+ 'subagent.spawned',
36
+ 'artifact.created',
37
+ ];
38
+ const MILESTONE_SET = new Set(MILESTONE_EVENTS);
39
+ export function tierForEvent(event) {
40
+ return MILESTONE_SET.has(event) ? 'milestone' : 'activity';
41
+ }
42
+ function activityPath(root, sessionId) {
43
+ const safe = sessionId.replace(/[^A-Za-z0-9._-]/g, '-');
44
+ if (!safe)
45
+ throw new Error(`Invalid activity session id: ${sessionId}`);
46
+ return path.join(root, `${safe}.jsonl`);
47
+ }
48
+ /**
49
+ * Append one event to a session's activity log. Primarily the Python hook
50
+ * writes these at runtime; this TS writer exists for tests and any in-process
51
+ * emitter. Uses O_APPEND so concurrent appends never interleave a line.
52
+ */
53
+ export function appendActivityEvent(event, root) {
54
+ const dir = root ?? getActivityDir();
55
+ fs.mkdirSync(dir, { recursive: true });
56
+ const record = {
57
+ v: event.v ?? 1,
58
+ tier: event.tier ?? tierForEvent(event.event),
59
+ ...event,
60
+ };
61
+ fs.appendFileSync(activityPath(dir, event.sessionId), `${JSON.stringify(record)}\n`, { mode: 0o644 });
62
+ }
63
+ function parseLine(line) {
64
+ const trimmed = line.trim();
65
+ if (!trimmed)
66
+ return undefined;
67
+ try {
68
+ const parsed = JSON.parse(trimmed);
69
+ if (!parsed.event || !parsed.sessionId || !parsed.ts)
70
+ return undefined;
71
+ return {
72
+ v: parsed.v ?? 1,
73
+ ts: parsed.ts,
74
+ event: parsed.event,
75
+ tier: parsed.tier ?? tierForEvent(parsed.event),
76
+ sessionId: parsed.sessionId,
77
+ mailboxId: parsed.mailboxId ?? parsed.sessionId,
78
+ host: parsed.host ?? 'unknown',
79
+ runtime: parsed.runtime ?? 'headless',
80
+ cwd: parsed.cwd,
81
+ agent: parsed.agent,
82
+ tool: parsed.tool,
83
+ detail: parsed.detail,
84
+ url: parsed.url,
85
+ };
86
+ }
87
+ catch {
88
+ return undefined; // skip corrupt / partial lines (fail-open reader)
89
+ }
90
+ }
91
+ /** Read the tail of a file as UTF-8, bounded to the last `maxBytes`. */
92
+ function readTail(file, maxBytes) {
93
+ let fd;
94
+ try {
95
+ fd = fs.openSync(file, 'r');
96
+ const size = fs.fstatSync(fd).size;
97
+ const start = size > maxBytes ? size - maxBytes : 0;
98
+ const len = size - start;
99
+ if (len <= 0)
100
+ return '';
101
+ const buf = Buffer.allocUnsafe(len);
102
+ fs.readSync(fd, buf, 0, len, start);
103
+ let text = buf.toString('utf-8');
104
+ // Drop a leading partial line when we started mid-file.
105
+ if (start > 0) {
106
+ const nl = text.indexOf('\n');
107
+ text = nl >= 0 ? text.slice(nl + 1) : '';
108
+ }
109
+ return text;
110
+ }
111
+ catch {
112
+ return '';
113
+ }
114
+ finally {
115
+ if (fd !== undefined)
116
+ fs.closeSync(fd);
117
+ }
118
+ }
119
+ /** Read all events for one session (bounded tail). */
120
+ export function readSessionActivity(sessionId, root, maxBytes = 256 * 1024) {
121
+ const dir = root ?? getActivityDir();
122
+ const text = readTail(activityPath(dir, sessionId), maxBytes);
123
+ const out = [];
124
+ for (const line of text.split('\n')) {
125
+ const ev = parseLine(line);
126
+ if (ev)
127
+ out.push(ev);
128
+ }
129
+ return out;
130
+ }
131
+ /** List session ids that have an activity log. */
132
+ export function listActivitySessions(root) {
133
+ const dir = root ?? getActivityDir();
134
+ try {
135
+ return fs.readdirSync(dir).filter(n => n.endsWith('.jsonl')).map(n => n.slice(0, -'.jsonl'.length));
136
+ }
137
+ catch {
138
+ return [];
139
+ }
140
+ }
141
+ /**
142
+ * Merge recent events across every session's log, newest first. Reads only the
143
+ * tail of each file, so cost scales with active sessions, not transcript size.
144
+ */
145
+ export function readRecentActivity(opts = {}) {
146
+ const dir = opts.root ?? getActivityDir();
147
+ const sinceMs = opts.sinceMs ?? 0;
148
+ const all = [];
149
+ for (const sessionId of listActivitySessions(dir)) {
150
+ for (const ev of readSessionActivity(sessionId, dir, opts.maxBytesPerSession)) {
151
+ const t = Date.parse(ev.ts);
152
+ if (Number.isFinite(t) && t >= sinceMs)
153
+ all.push(ev);
154
+ }
155
+ }
156
+ all.sort((a, b) => Date.parse(b.ts) - Date.parse(a.ts));
157
+ return typeof opts.limit === 'number' ? all.slice(0, opts.limit) : all;
158
+ }
159
+ /**
160
+ * Split a chronological event list into individual milestones plus a
161
+ * count map for the routine (tier-2) events, so a reader shows recognizable
162
+ * deliverables in full and collapses the noise.
163
+ */
164
+ export function collapseActivity(events) {
165
+ const milestones = [];
166
+ const counts = {};
167
+ let subagentCount = 0;
168
+ for (const ev of events) {
169
+ if (tierForEvent(ev.event) === 'milestone') {
170
+ milestones.push(ev);
171
+ if (ev.event === 'subagent.spawned')
172
+ subagentCount += 1;
173
+ }
174
+ else {
175
+ counts[ev.event] = (counts[ev.event] ?? 0) + 1;
176
+ }
177
+ }
178
+ return { milestones, counts, subagentCount };
179
+ }
180
+ // ---------------------------------------------------------------------------
181
+ // Bridge into the unified event stream (lib/event-stream.ts)
182
+ // ---------------------------------------------------------------------------
183
+ /**
184
+ * Normalize one activity event into the shared {@link EventRecord} shape so the
185
+ * agent-semantic stream reads through the same reader as operational events.
186
+ * `module` is stamped `activity` so `--module` filters partition the two cleanly.
187
+ */
188
+ export function activityEventToRecord(ev) {
189
+ return {
190
+ ts: ev.ts,
191
+ tz: '',
192
+ tzName: '',
193
+ hostname: ev.host,
194
+ platform: process.platform,
195
+ arch: process.arch,
196
+ pid: 0,
197
+ ppid: 0,
198
+ event: ev.event,
199
+ level: 'info',
200
+ caller: 'hook',
201
+ session: ev.sessionId,
202
+ osUser: ev.agent ?? 'agent',
203
+ transport: 'local',
204
+ // payload
205
+ agent: ev.agent,
206
+ sessionId: ev.sessionId,
207
+ cwd: ev.cwd,
208
+ module: 'activity',
209
+ tool: ev.tool,
210
+ detail: ev.detail,
211
+ url: ev.url,
212
+ tier: ev.tier,
213
+ };
214
+ }
215
+ /** Read recent activity across sessions as unified {@link EventRecord}s. */
216
+ export function readActivityAsEventRecords(opts = {}) {
217
+ return readRecentActivity(opts).map(activityEventToRecord);
218
+ }
219
+ // ---------------------------------------------------------------------------
220
+ // Rendering (shared by `agents activity` and `agents feed`)
221
+ // ---------------------------------------------------------------------------
222
+ /** Glyph + color + human label per event, so the lane reads at a glance. */
223
+ export const EVENT_STYLE = {
224
+ 'plan.created': { glyph: '◆', color: chalk.cyan, label: 'plan created' },
225
+ 'pr.opened': { glyph: '⇡', color: chalk.green, label: 'PR opened' },
226
+ 'pr.merged': { glyph: '✔', color: chalk.green, label: 'PR merged' },
227
+ 'worktree.created': { glyph: '⌥', color: chalk.blue, label: 'worktree created' },
228
+ 'worktree.removed': { glyph: '⌦', color: chalk.gray, label: 'worktree removed' },
229
+ 'commit.created': { glyph: '●', color: chalk.yellow, label: 'commit' },
230
+ 'pushed': { glyph: '↥', color: chalk.yellow, label: 'pushed' },
231
+ 'subagent.spawned': { glyph: '⑂', color: chalk.magenta, label: 'sub-agent spawned' },
232
+ 'artifact.created': { glyph: '▤', color: chalk.cyan, label: 'artifact' },
233
+ 'file.edited': { glyph: '·', color: chalk.gray, label: 'file edited' },
234
+ };
235
+ export function styleForEvent(event) {
236
+ return EVENT_STYLE[event] ?? { glyph: '•', color: chalk.white, label: event };
237
+ }
238
+ /** One rendered activity line: ` <rel> [host] <glyph label> detail url`. */
239
+ export function formatActivityLine(ev, opts = {}) {
240
+ const s = styleForEvent(ev.event);
241
+ const host = opts.showHost && ev.host && ev.host !== 'unknown' ? chalk.gray(`[${ev.host}] `) : '';
242
+ const label = s.color(`${s.glyph} ${s.label}`);
243
+ const detail = ev.detail ? ` ${truncate(ev.detail, 60)}` : '';
244
+ const url = ev.url ? chalk.gray(` ${ev.url}`) : '';
245
+ const when = chalk.gray(relTime(ev.ts).padStart(7));
246
+ return ` ${when} ${host}${label}${detail}${url}`;
247
+ }
248
+ // ---------------------------------------------------------------------------
249
+ // Hook installation
250
+ // ---------------------------------------------------------------------------
251
+ /**
252
+ * The activity-log hook (Python), sibling to 10-feed-publish.py. Classifies
253
+ * PreToolUse/PostToolUse payloads into activity events and appends one JSONL
254
+ * line per event to ~/.agents/.history/activity/<sessionId>.jsonl.
255
+ *
256
+ * Matcher-gated to mutating/milestone tools (Bash|Task|ExitPlanMode|Write|
257
+ * Edit|MultiEdit) so read-only tools never pay the hook cost. Fail-open: any
258
+ * error is swallowed so a logging hiccup never blocks a tool call.
259
+ */
260
+ export const ACTIVITY_LOG_HOOK_SCRIPT = `#!/usr/bin/env python3
261
+ """Append agent-activity events for \`agents feed\` / \`agents activity\`.
262
+
263
+ Bound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,
264
+ MultiEdit). One append-only file per session; read-only tools never trigger it
265
+ because the manifest matcher excludes them.
266
+
267
+ Sub-agent gate: when the payload carries \`agent_type\`, this is a Task/Agent
268
+ sub-agent -- skip so only the top-level agent logs its own activity.
269
+
270
+ Fail-open: ANY error is swallowed so a logging hiccup never blocks a tool call.
271
+ """
272
+ import os
273
+ import re
274
+ import sys
275
+ import json
276
+ import shlex
277
+ import socket
278
+ from datetime import datetime, timezone
279
+
280
+ MAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log
281
+ MILESTONE_EVENTS = {
282
+ "plan.created", "pr.opened", "pr.merged", "worktree.created",
283
+ "worktree.removed", "commit.created", "pushed", "subagent.spawned",
284
+ "artifact.created",
285
+ }
286
+
287
+ # Deliverable file types + locations -- a Write here is a recognizable artifact
288
+ # (an HTML plan, a PDF report, a rendered image), not a routine code edit.
289
+ ARTIFACT_EXTS = {
290
+ ".html", ".htm", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".svg",
291
+ ".webp", ".mp4", ".mov", ".webm", ".csv", ".xlsx", ".pptx", ".docx",
292
+ }
293
+ ARTIFACT_DIR_HINTS = ("/tmp/", "/downloads/", "/.agents/artifacts/")
294
+
295
+
296
+ def is_artifact(file_path):
297
+ low = (file_path or "").lower()
298
+ if os.path.splitext(low)[1] in ARTIFACT_EXTS:
299
+ return True
300
+ return any(hint in low for hint in ARTIFACT_DIR_HINTS)
301
+
302
+
303
+ def first_line(text, limit=140):
304
+ for raw in (text or "").splitlines():
305
+ s = raw.strip().lstrip("#").strip()
306
+ if s:
307
+ return s[:limit]
308
+ return ""
309
+
310
+
311
+ def _subcommand(tokens, tool):
312
+ """First non-flag token after \`tool\`, i.e. its subcommand (skips -C <path>,
313
+ -c <cfg>, and other leading flags). None if \`tool\` isn't the invoked command."""
314
+ try:
315
+ i = tokens.index(tool) + 1
316
+ except ValueError:
317
+ return None, []
318
+ while i < len(tokens):
319
+ t = tokens[i]
320
+ if t in ("-C", "-c", "--git-dir", "--work-tree"):
321
+ i += 2 # flag that consumes the next token
322
+ continue
323
+ if t.startswith("-"):
324
+ i += 1
325
+ continue
326
+ return t, tokens[i + 1:]
327
+ return None, []
328
+
329
+
330
+ def classify_bash(command):
331
+ """Return the milestone event for a git/gh command, else None. Tokenizes so a
332
+ path like \`git diff -- src/commit.ts\` is not mistaken for a commit."""
333
+ try:
334
+ tokens = shlex.split(command or "")
335
+ except Exception:
336
+ tokens = (command or "").split()
337
+
338
+ verb, rest = _subcommand(tokens, "git")
339
+ if verb == "worktree":
340
+ sub = rest[0] if rest else ""
341
+ if sub == "add":
342
+ return "worktree.created"
343
+ if sub == "remove":
344
+ return "worktree.removed"
345
+ elif verb == "commit":
346
+ return "commit.created"
347
+ elif verb == "push":
348
+ return "pushed"
349
+
350
+ verb, rest = _subcommand(tokens, "gh")
351
+ if verb == "pr" and rest:
352
+ if rest[0] == "create":
353
+ return "pr.opened"
354
+ if rest[0] == "merge":
355
+ return "pr.merged"
356
+ return None
357
+
358
+
359
+ def extract_url(tool_response):
360
+ """Pull the first https URL out of a Bash tool response (stdout)."""
361
+ text = ""
362
+ if isinstance(tool_response, dict):
363
+ text = str(tool_response.get("stdout") or tool_response.get("output") or "")
364
+ elif isinstance(tool_response, str):
365
+ text = tool_response
366
+ m = re.search(r"https?://\\S+", text)
367
+ return m.group(0).rstrip(").,") if m else None
368
+
369
+
370
+ def build_event(payload, hook_event):
371
+ tool_name = payload.get("tool_name", "")
372
+ tool_input = payload.get("tool_input", {}) or {}
373
+ tool_response = payload.get("tool_response", {})
374
+
375
+ event = None
376
+ detail = None
377
+ url = None
378
+
379
+ if hook_event == "PreToolUse":
380
+ if tool_name == "ExitPlanMode":
381
+ event = "plan.created"
382
+ detail = first_line(tool_input.get("plan", "")) or "plan presented"
383
+ elif tool_name == "Task":
384
+ event = "subagent.spawned"
385
+ role = tool_input.get("subagent_type") or "agent"
386
+ desc = tool_input.get("description") or tool_input.get("prompt") or ""
387
+ detail = (role + ": " + first_line(desc)).strip(": ").strip()
388
+ elif hook_event == "PostToolUse":
389
+ if tool_name == "Bash":
390
+ event = classify_bash(tool_input.get("command", ""))
391
+ if event:
392
+ detail = first_line(tool_input.get("command", ""))
393
+ url = extract_url(tool_response)
394
+ elif tool_name in ("Write", "Edit", "MultiEdit"):
395
+ fp = tool_input.get("file_path") or tool_input.get("path") or ""
396
+ # A freshly-written deliverable is a milestone; edits and code
397
+ # writes stay routine and collapse to a count.
398
+ if tool_name == "Write" and is_artifact(fp):
399
+ event = "artifact.created"
400
+ else:
401
+ event = "file.edited"
402
+ detail = os.path.basename(fp) if fp else tool_name
403
+
404
+ if not event:
405
+ return None, tool_name
406
+
407
+ tier = "milestone" if event in MILESTONE_EVENTS else "activity"
408
+ record = {
409
+ "v": 1,
410
+ "ts": datetime.now(timezone.utc).isoformat(),
411
+ "event": event,
412
+ "tier": tier,
413
+ }
414
+ if detail:
415
+ record["detail"] = detail
416
+ if url:
417
+ record["url"] = url
418
+ record["tool"] = tool_name
419
+ return record, tool_name
420
+
421
+
422
+ def main():
423
+ raw = sys.stdin.read()
424
+ try:
425
+ payload = json.loads(raw) if raw.strip() else {}
426
+ except Exception:
427
+ return
428
+
429
+ # Sub-agent gate -- only the top-level agent logs.
430
+ if payload.get("agent_type"):
431
+ return
432
+
433
+ session_id = payload.get("session_id", "")
434
+ if not session_id:
435
+ return
436
+
437
+ hook_event = payload.get("hook_event_name", "")
438
+ record, tool_name = build_event(payload, hook_event)
439
+ if not record:
440
+ return
441
+
442
+ safe_session = re.sub(r"[^A-Za-z0-9._-]", "-", session_id) or "unknown"
443
+ home = os.environ.get("HOME") or os.path.expanduser("~")
444
+ activity_dir = os.path.join(home, ".agents", ".history", "activity")
445
+ target = os.path.join(activity_dir, safe_session + ".jsonl")
446
+
447
+ # Identity (mirrors 10-feed-publish.py).
448
+ mailbox_id = os.path.basename(
449
+ os.environ.get("AGENTS_MAILBOX_DIR", "").rstrip("/")
450
+ ) or session_id
451
+ hostname = os.environ.get("AGENTS_SYNC_MACHINE_ID") or socket.gethostname()
452
+ host = re.sub(r"[^a-z0-9_-]", "-", hostname.split(".")[0].strip().lower()) or "unknown"
453
+
454
+ record["sessionId"] = session_id
455
+ record["mailboxId"] = mailbox_id
456
+ record["host"] = host
457
+ record["runtime"] = os.environ.get("AGENTS_RUNTIME", "headless")
458
+ cwd = payload.get("cwd") or os.environ.get("AGENTS_CWD")
459
+ if cwd:
460
+ record["cwd"] = cwd
461
+ agent = os.environ.get("AGENTS_AGENT_NAME") or "claude"
462
+ record["agent"] = agent
463
+
464
+ try:
465
+ os.makedirs(activity_dir, exist_ok=True)
466
+ # Cap growth: once over the size limit, keep only milestones.
467
+ if record["tier"] != "milestone":
468
+ try:
469
+ if os.path.getsize(target) > MAX_LOG_BYTES:
470
+ return
471
+ except OSError:
472
+ pass
473
+ with open(target, "a") as f:
474
+ f.write(json.dumps(record) + "\\n")
475
+ except Exception:
476
+ pass # fail open
477
+
478
+
479
+ if __name__ == "__main__":
480
+ try:
481
+ main()
482
+ except Exception:
483
+ pass # fail open
484
+ `;
485
+ /** Hook manifest entries (agents.yaml shape) for the activity-log hook. */
486
+ export const ACTIVITY_HOOK_DEFINITIONS = {
487
+ 'activity-log-intent': {
488
+ agents: ['claude'],
489
+ events: ['PreToolUse'],
490
+ matcher: 'ExitPlanMode|Task',
491
+ script: '11-activity-log.py',
492
+ timeout: 5,
493
+ },
494
+ 'activity-log-result': {
495
+ agents: ['claude'],
496
+ events: ['PostToolUse'],
497
+ matcher: 'Bash|Write|Edit|MultiEdit',
498
+ script: '11-activity-log.py',
499
+ timeout: 5,
500
+ },
501
+ };
502
+ /**
503
+ * Install the activity-log hook script into the user hooks dir and add its
504
+ * manifest entries to the user agents.yaml. Mirrors {@link ensureFeedPublishHook}
505
+ * in feed.ts -- idempotent, and never writes the read-only system repo.
506
+ */
507
+ export function ensureActivityLogHook(userAgentsDir = getUserAgentsDir()) {
508
+ try {
509
+ const hooksDir = path.join(userAgentsDir, 'hooks');
510
+ const scriptPath = path.join(hooksDir, '11-activity-log.py');
511
+ fs.mkdirSync(hooksDir, { recursive: true });
512
+ let installed = false;
513
+ if (!fs.existsSync(scriptPath) || fs.readFileSync(scriptPath, 'utf-8') !== ACTIVITY_LOG_HOOK_SCRIPT) {
514
+ const tmpScript = `${scriptPath}.${process.pid}.tmp`;
515
+ fs.writeFileSync(tmpScript, ACTIVITY_LOG_HOOK_SCRIPT, { mode: 0o755 });
516
+ fs.renameSync(tmpScript, scriptPath);
517
+ installed = true;
518
+ }
519
+ const agentsYamlPath = path.join(userAgentsDir, 'agents.yaml');
520
+ const yamlDoc = fs.existsSync(agentsYamlPath)
521
+ ? yaml.parseDocument(fs.readFileSync(agentsYamlPath, 'utf-8'))
522
+ : new yaml.Document({});
523
+ if (yamlDoc.errors.length > 0) {
524
+ throw new Error(`Cannot install activity hook: ${agentsYamlPath} is invalid YAML`);
525
+ }
526
+ for (const [name, definition] of Object.entries(ACTIVITY_HOOK_DEFINITIONS)) {
527
+ if (!yamlDoc.getIn(['hooks', name])) {
528
+ yamlDoc.setIn(['hooks', name], definition);
529
+ installed = true;
530
+ }
531
+ }
532
+ if (installed) {
533
+ const tmpYaml = `${agentsYamlPath}.${process.pid}.tmp`;
534
+ fs.writeFileSync(tmpYaml, String(yamlDoc));
535
+ fs.renameSync(tmpYaml, agentsYamlPath);
536
+ }
537
+ return { installed };
538
+ }
539
+ catch (err) {
540
+ return { installed: false, error: err.message };
541
+ }
542
+ }
@@ -71,6 +71,17 @@ export declare function getCliPath(agentId: AgentId): Promise<string | null>;
71
71
  * back to a plain PATH lookup.
72
72
  */
73
73
  export declare function getCliState(agentId: AgentId): Promise<CliState>;
74
+ /**
75
+ * Resolve the agent's OWN install — the one agents-cli does not manage — by plain
76
+ * PATH lookup, ignoring the version dirs entirely.
77
+ *
78
+ * Callers that specifically mean "the user's own globally-installed CLI" must use
79
+ * this rather than `getCliState`, whose managed fast path reports an installed
80
+ * version (any version dir, in readdir order) and would therefore hand back an
81
+ * isolated copy — a copy that is deliberately unreachable from PATH — labelled as
82
+ * the global install.
83
+ */
84
+ export declare function getUnmanagedCliState(agentId: AgentId): Promise<CliState>;
74
85
  /** Resolve CLI state for all registered agents in parallel. */
75
86
  export declare function getAllCliStates(): Promise<Partial<Record<AgentId, CliState>>>;
76
87
  /** Info about an existing unmanaged agent installation. */
@@ -230,6 +241,30 @@ export declare function antigravityOsKeyringProbe(platform?: NodeJS.Platform): {
230
241
  } | null;
231
242
  /** @internal test hook — clear the per-process keyring probe cache. */
232
243
  export declare function __resetAntigravityKeychainCacheForTest(): void;
244
+ /**
245
+ * Whether a Claude version home's credential file is present but carries no
246
+ * token — the "must have a real credential" floor (see
247
+ * `isValidOpenCodeCredential`) applied to claude.
248
+ *
249
+ * A FAILED OAuth refresh leaves exactly this state behind: Claude Code rewrites
250
+ * `.claude/.credentials.json` with `accessToken: ""`, `refreshToken: ""` and
251
+ * `expiresAt: 0`, keeping only the descriptive fields (`subscriptionType`,
252
+ * `rateLimitTier`, `refreshTokenExpiresAt`). Everything we derive from
253
+ * `.claude.json` — email, plan — still looks healthy, so the install reported
254
+ * `signedIn: true`, `agents view` drew usage bars for it, and balanced rotation
255
+ * (whose `authValid` is just "email present") kept picking it — every pick dying
256
+ * at spawn on "OAuth session expired and could not be refreshed".
257
+ *
258
+ * Only decidable off macOS: there the login Keychain is the canonical store and
259
+ * this file is not authoritative, and probing the Keychain would raise an
260
+ * authorization sheet per installed version on every `agents run` — the reason
261
+ * rotation stopped calling `isClaudeAuthValid` at all. Off macOS the file IS the
262
+ * only store, so a token-less file is proof of signed-out. `platform` is a
263
+ * parameter so both branches are testable on any host.
264
+ *
265
+ * Sync, no Keychain, no network — safe on the `agents run` hot path.
266
+ */
267
+ export declare function isClaudeCredentialFileBlank(base: string, platform?: NodeJS.Platform): boolean;
233
268
  export declare function getAccountInfo(agentId: AgentId, home?: string): Promise<AccountInfo>;
234
269
  /**
235
270
  * Determine when the agent was last used by checking session file mtimes,