@phnx-labs/agents-cli 1.20.73 → 1.20.76
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 +989 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +31 -1
- package/dist/commands/exec.js +31 -3
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +87 -9
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/setup.js +6 -1
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +51 -11
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +99 -31
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +56 -0
- package/dist/lib/agents.js +133 -7
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +60 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +25 -0
- package/dist/lib/import.js +107 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +27 -1
- package/dist/lib/shims.js +88 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +15 -36
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +48 -2
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +22 -2
- package/dist/lib/versions.js +98 -10
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/lib/activity.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { EventRecord } from './events.js';
|
|
2
2
|
/** Recognizable milestone events, ordered first in any activity lane. */
|
|
3
|
-
export type MilestoneEvent = 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created'
|
|
3
|
+
export type MilestoneEvent = 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created'
|
|
4
|
+
/** Deliberate agent-authored progress post (`agents feed post`). */
|
|
5
|
+
| 'status.posted';
|
|
4
6
|
/** Routine activity events, collapsed to counts by readers. */
|
|
5
7
|
export type ActivityKind = 'file.edited';
|
|
6
8
|
export type ActivityEventKind = MilestoneEvent | ActivityKind;
|
|
@@ -29,12 +31,20 @@ export interface ActivityEvent {
|
|
|
29
31
|
cwd?: string;
|
|
30
32
|
/** Agent that produced the event (claude, codex, ...). */
|
|
31
33
|
agent?: string;
|
|
32
|
-
/** Tool that triggered the event (Bash, Task, ExitPlanMode, ...). */
|
|
34
|
+
/** Tool that triggered the event (Bash, Task, ExitPlanMode, feed.post, ...). */
|
|
33
35
|
tool?: string;
|
|
34
|
-
/** One-line human summary (plan title, PR command, sub-agent role). */
|
|
36
|
+
/** One-line human summary (plan title, PR command, sub-agent role, status text). */
|
|
35
37
|
detail?: string;
|
|
36
38
|
/** Extracted URL when the event has one (e.g. the opened PR). */
|
|
37
39
|
url?: string;
|
|
40
|
+
/** Auto-stamped process identity for deliberate posts (from pid registry / env). */
|
|
41
|
+
pid?: number;
|
|
42
|
+
/** Spawn-time join key (`AGENT_LAUNCH_ID`) when known. */
|
|
43
|
+
launchId?: string;
|
|
44
|
+
/** Factory terminal id when the launch inherited one. */
|
|
45
|
+
terminalId?: string;
|
|
46
|
+
/** `$TMUX_PANE` at launch when recorded. */
|
|
47
|
+
tmuxPane?: string;
|
|
38
48
|
}
|
|
39
49
|
/**
|
|
40
50
|
* Append one event to a session's activity log. Primarily the Python hook
|
|
@@ -110,7 +120,7 @@ export declare function formatActivityLine(ev: ActivityEvent, opts?: {
|
|
|
110
120
|
* Edit|MultiEdit) so read-only tools never pay the hook cost. Fail-open: any
|
|
111
121
|
* error is swallowed so a logging hiccup never blocks a tool call.
|
|
112
122
|
*/
|
|
113
|
-
export declare const ACTIVITY_LOG_HOOK_SCRIPT = "#!/usr/bin/env python3\n\"\"\"Append agent-activity events for `agents feed` / `agents activity`.\n\nBound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,\nMultiEdit). One append-only file per session; read-only tools never trigger it\nbecause the manifest matcher excludes them.\n\nSub-agent gate: when the payload carries `agent_type`, this is a Task/Agent\nsub-agent -- skip so only the top-level agent logs its own activity.\n\nFail-open: ANY error is swallowed so a logging hiccup never blocks a tool call.\n\"\"\"\nimport os\nimport re\nimport sys\nimport json\nimport shlex\nimport socket\nfrom datetime import datetime, timezone\n\nMAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log\nMILESTONE_EVENTS = {\n \"plan.created\", \"pr.opened\", \"pr.merged\", \"worktree.created\",\n \"worktree.removed\", \"commit.created\", \"pushed\", \"subagent.spawned\",\n \"artifact.created\",\n}\n\n# Deliverable file types + locations -- a Write here is a recognizable artifact\n# (an HTML plan, a PDF report, a rendered image), not a routine code edit.\nARTIFACT_EXTS = {\n \".html\", \".htm\", \".pdf\", \".png\", \".jpg\", \".jpeg\", \".gif\", \".svg\",\n \".webp\", \".mp4\", \".mov\", \".webm\", \".csv\", \".xlsx\", \".pptx\", \".docx\",\n}\nARTIFACT_DIR_HINTS = (\"/tmp/\", \"/downloads/\", \"/.agents/artifacts/\")\n\n\ndef is_artifact(file_path):\n low = (file_path or \"\").lower()\n if os.path.splitext(low)[1] in ARTIFACT_EXTS:\n return True\n return any(hint in low for hint in ARTIFACT_DIR_HINTS)\n\n\ndef first_line(text, limit=140):\n for raw in (text or \"\").splitlines():\n s = raw.strip().lstrip(\"#\").strip()\n if s:\n return s[:limit]\n return \"\"\n\n\ndef _subcommand(tokens, tool):\n \"\"\"First non-flag token after `tool`, i.e. its subcommand (skips -C <path>,\n -c <cfg>, and other leading flags). None if `tool` isn't the invoked command.\"\"\"\n try:\n i = tokens.index(tool) + 1\n except ValueError:\n return None, []\n while i < len(tokens):\n t = tokens[i]\n if t in (\"-C\", \"-c\", \"--git-dir\", \"--work-tree\"):\n i += 2 # flag that consumes the next token\n continue\n if t.startswith(\"-\"):\n i += 1\n continue\n return t, tokens[i + 1:]\n return None, []\n\n\ndef classify_bash(command):\n \"\"\"Return the milestone event for a git/gh command, else None. Tokenizes so a\n path like `git diff -- src/commit.ts` is not mistaken for a commit.\"\"\"\n try:\n tokens = shlex.split(command or \"\")\n except Exception:\n tokens = (command or \"\").split()\n\n verb, rest = _subcommand(tokens, \"git\")\n if verb == \"worktree\":\n sub = rest[0] if rest else \"\"\n if sub == \"add\":\n return \"worktree.created\"\n if sub == \"remove\":\n return \"worktree.removed\"\n elif verb == \"commit\":\n return \"commit.created\"\n elif verb == \"push\":\n return \"pushed\"\n\n verb, rest = _subcommand(tokens, \"gh\")\n if verb == \"pr\" and rest:\n if rest[0] == \"create\":\n return \"pr.opened\"\n if rest[0] == \"merge\":\n return \"pr.merged\"\n return None\n\n\ndef extract_url(tool_response):\n \"\"\"Pull the first https URL out of a Bash tool response (stdout).\"\"\"\n text = \"\"\n if isinstance(tool_response, dict):\n text = str(tool_response.get(\"stdout\") or tool_response.get(\"output\") or \"\")\n elif isinstance(tool_response, str):\n text = tool_response\n m = re.search(r\"https?://\\S+\", text)\n return m.group(0).rstrip(\").,\") if m else None\n\n\ndef build_event(payload, hook_event):\n tool_name = payload.get(\"tool_name\", \"\")\n tool_input = payload.get(\"tool_input\", {}) or {}\n tool_response = payload.get(\"tool_response\", {})\n\n event = None\n detail = None\n url = None\n\n if hook_event == \"PreToolUse\":\n if tool_name == \"ExitPlanMode\":\n event = \"plan.created\"\n detail = first_line(tool_input.get(\"plan\", \"\")) or \"plan presented\"\n elif tool_name == \"Task\":\n event = \"subagent.spawned\"\n role = tool_input.get(\"subagent_type\") or \"agent\"\n desc = tool_input.get(\"description\") or tool_input.get(\"prompt\") or \"\"\n detail = (role + \": \" + first_line(desc)).strip(\": \").strip()\n elif hook_event == \"PostToolUse\":\n if tool_name == \"Bash\":\n event = classify_bash(tool_input.get(\"command\", \"\"))\n if event:\n detail = first_line(tool_input.get(\"command\", \"\"))\n url = extract_url(tool_response)\n elif tool_name in (\"Write\", \"Edit\", \"MultiEdit\"):\n fp = tool_input.get(\"file_path\") or tool_input.get(\"path\") or \"\"\n # A freshly-written deliverable is a milestone; edits and code\n # writes stay routine and collapse to a count.\n if tool_name == \"Write\" and is_artifact(fp):\n event = \"artifact.created\"\n else:\n event = \"file.edited\"\n detail = os.path.basename(fp) if fp else tool_name\n\n if not event:\n return None, tool_name\n\n tier = \"milestone\" if event in MILESTONE_EVENTS else \"activity\"\n record = {\n \"v\": 1,\n \"ts\": datetime.now(timezone.utc).isoformat(),\n \"event\": event,\n \"tier\": tier,\n }\n if detail:\n record[\"detail\"] = detail\n if url:\n record[\"url\"] = url\n record[\"tool\"] = tool_name\n return record, tool_name\n\n\ndef main():\n raw = sys.stdin.read()\n try:\n payload = json.loads(raw) if raw.strip() else {}\n except Exception:\n return\n\n # Sub-agent gate -- only the top-level agent logs.\n if payload.get(\"agent_type\"):\n return\n\n session_id = payload.get(\"session_id\", \"\")\n if not session_id:\n return\n\n hook_event = payload.get(\"hook_event_name\", \"\")\n record, tool_name = build_event(payload, hook_event)\n if not record:\n return\n\n safe_session = re.sub(r\"[^A-Za-z0-9._-]\", \"-\", session_id) or \"unknown\"\n home = os.environ.get(\"HOME\") or os.path.expanduser(\"~\")\n activity_dir = os.path.join(home, \".agents\", \".history\", \"activity\")\n target = os.path.join(activity_dir, safe_session + \".jsonl\")\n\n # Identity (mirrors 10-feed-publish.py).\n mailbox_id = os.path.basename(\n os.environ.get(\"AGENTS_MAILBOX_DIR\", \"\").rstrip(\"/\")\n ) or session_id\n hostname = os.environ.get(\"AGENTS_SYNC_MACHINE_ID\") or socket.gethostname()\n host = re.sub(r\"[^a-z0-9_-]\", \"-\", hostname.split(\".\")[0].strip().lower()) or \"unknown\"\n\n record[\"sessionId\"] = session_id\n record[\"mailboxId\"] = mailbox_id\n record[\"host\"] = host\n record[\"runtime\"] = os.environ.get(\"AGENTS_RUNTIME\", \"headless\")\n cwd = payload.get(\"cwd\") or os.environ.get(\"AGENTS_CWD\")\n if cwd:\n record[\"cwd\"] = cwd\n agent = os.environ.get(\"AGENTS_AGENT_NAME\") or \"claude\"\n record[\"agent\"] = agent\n\n try:\n os.makedirs(activity_dir, exist_ok=True)\n # Cap growth: once over the size limit, keep only milestones.\n if record[\"tier\"] != \"milestone\":\n try:\n if os.path.getsize(target) > MAX_LOG_BYTES:\n return\n except OSError:\n pass\n with open(target, \"a\") as f:\n f.write(json.dumps(record) + \"\\n\")\n except Exception:\n pass # fail open\n\n\nif __name__ == \"__main__\":\n try:\n main()\n except Exception:\n pass # fail open\n";
|
|
123
|
+
export declare const ACTIVITY_LOG_HOOK_SCRIPT = "#!/usr/bin/env python3\n\"\"\"Append agent-activity events for `agents feed` / `agents activity`.\n\nBound to PreToolUse (ExitPlanMode, Task) and PostToolUse (Bash, Write, Edit,\nMultiEdit). One append-only file per session; read-only tools never trigger it\nbecause the manifest matcher excludes them.\n\nSub-agent gate: when the payload carries `agent_type`, this is a Task/Agent\nsub-agent -- skip so only the top-level agent logs its own activity.\n\nFail-open: ANY error is swallowed so a logging hiccup never blocks a tool call.\n\"\"\"\nimport os\nimport re\nimport sys\nimport json\nimport shlex\nimport socket\nfrom datetime import datetime, timezone\n\nMAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log\nMILESTONE_EVENTS = {\n \"plan.created\", \"pr.opened\", \"pr.merged\", \"worktree.created\",\n \"worktree.removed\", \"commit.created\", \"pushed\", \"subagent.spawned\",\n \"artifact.created\", \"status.posted\",\n}\n\n# Deliverable file types + locations -- a Write here is a recognizable artifact\n# (an HTML plan, a PDF report, a rendered image), not a routine code edit.\nARTIFACT_EXTS = {\n \".html\", \".htm\", \".pdf\", \".png\", \".jpg\", \".jpeg\", \".gif\", \".svg\",\n \".webp\", \".mp4\", \".mov\", \".webm\", \".csv\", \".xlsx\", \".pptx\", \".docx\",\n}\nARTIFACT_DIR_HINTS = (\"/tmp/\", \"/downloads/\", \"/.agents/artifacts/\")\n\n\ndef is_artifact(file_path):\n low = (file_path or \"\").lower()\n if os.path.splitext(low)[1] in ARTIFACT_EXTS:\n return True\n return any(hint in low for hint in ARTIFACT_DIR_HINTS)\n\n\ndef first_line(text, limit=140):\n for raw in (text or \"\").splitlines():\n s = raw.strip().lstrip(\"#\").strip()\n if s:\n return s[:limit]\n return \"\"\n\n\ndef _subcommand(tokens, tool):\n \"\"\"First non-flag token after `tool`, i.e. its subcommand (skips -C <path>,\n -c <cfg>, and other leading flags). None if `tool` isn't the invoked command.\"\"\"\n try:\n i = tokens.index(tool) + 1\n except ValueError:\n return None, []\n while i < len(tokens):\n t = tokens[i]\n if t in (\"-C\", \"-c\", \"--git-dir\", \"--work-tree\"):\n i += 2 # flag that consumes the next token\n continue\n if t.startswith(\"-\"):\n i += 1\n continue\n return t, tokens[i + 1:]\n return None, []\n\n\ndef classify_bash(command):\n \"\"\"Return the milestone event for a git/gh command, else None. Tokenizes so a\n path like `git diff -- src/commit.ts` is not mistaken for a commit.\"\"\"\n try:\n tokens = shlex.split(command or \"\")\n except Exception:\n tokens = (command or \"\").split()\n\n verb, rest = _subcommand(tokens, \"git\")\n if verb == \"worktree\":\n sub = rest[0] if rest else \"\"\n if sub == \"add\":\n return \"worktree.created\"\n if sub == \"remove\":\n return \"worktree.removed\"\n elif verb == \"commit\":\n return \"commit.created\"\n elif verb == \"push\":\n return \"pushed\"\n\n verb, rest = _subcommand(tokens, \"gh\")\n if verb == \"pr\" and rest:\n if rest[0] == \"create\":\n return \"pr.opened\"\n if rest[0] == \"merge\":\n return \"pr.merged\"\n return None\n\n\ndef extract_url(tool_response):\n \"\"\"Pull the first https URL out of a Bash tool response (stdout).\"\"\"\n text = \"\"\n if isinstance(tool_response, dict):\n text = str(tool_response.get(\"stdout\") or tool_response.get(\"output\") or \"\")\n elif isinstance(tool_response, str):\n text = tool_response\n m = re.search(r\"https?://\\S+\", text)\n return m.group(0).rstrip(\").,\") if m else None\n\n\ndef build_event(payload, hook_event):\n tool_name = payload.get(\"tool_name\", \"\")\n tool_input = payload.get(\"tool_input\", {}) or {}\n tool_response = payload.get(\"tool_response\", {})\n\n event = None\n detail = None\n url = None\n\n if hook_event == \"PreToolUse\":\n if tool_name == \"ExitPlanMode\":\n event = \"plan.created\"\n detail = first_line(tool_input.get(\"plan\", \"\")) or \"plan presented\"\n elif tool_name == \"Task\":\n event = \"subagent.spawned\"\n role = tool_input.get(\"subagent_type\") or \"agent\"\n desc = tool_input.get(\"description\") or tool_input.get(\"prompt\") or \"\"\n detail = (role + \": \" + first_line(desc)).strip(\": \").strip()\n elif hook_event == \"PostToolUse\":\n if tool_name == \"Bash\":\n event = classify_bash(tool_input.get(\"command\", \"\"))\n if event:\n detail = first_line(tool_input.get(\"command\", \"\"))\n url = extract_url(tool_response)\n elif tool_name in (\"Write\", \"Edit\", \"MultiEdit\"):\n fp = tool_input.get(\"file_path\") or tool_input.get(\"path\") or \"\"\n # A freshly-written deliverable is a milestone; edits and code\n # writes stay routine and collapse to a count.\n if tool_name == \"Write\" and is_artifact(fp):\n event = \"artifact.created\"\n else:\n event = \"file.edited\"\n detail = os.path.basename(fp) if fp else tool_name\n\n if not event:\n return None, tool_name\n\n tier = \"milestone\" if event in MILESTONE_EVENTS else \"activity\"\n record = {\n \"v\": 1,\n \"ts\": datetime.now(timezone.utc).isoformat(),\n \"event\": event,\n \"tier\": tier,\n }\n if detail:\n record[\"detail\"] = detail\n if url:\n record[\"url\"] = url\n record[\"tool\"] = tool_name\n return record, tool_name\n\n\ndef main():\n raw = sys.stdin.read()\n try:\n payload = json.loads(raw) if raw.strip() else {}\n except Exception:\n return\n\n # Sub-agent gate -- only the top-level agent logs.\n if payload.get(\"agent_type\"):\n return\n\n session_id = payload.get(\"session_id\", \"\")\n if not session_id:\n return\n\n hook_event = payload.get(\"hook_event_name\", \"\")\n record, tool_name = build_event(payload, hook_event)\n if not record:\n return\n\n safe_session = re.sub(r\"[^A-Za-z0-9._-]\", \"-\", session_id) or \"unknown\"\n home = os.environ.get(\"HOME\") or os.path.expanduser(\"~\")\n activity_dir = os.path.join(home, \".agents\", \".history\", \"activity\")\n target = os.path.join(activity_dir, safe_session + \".jsonl\")\n\n # Identity (mirrors 10-feed-publish.py).\n mailbox_id = os.path.basename(\n os.environ.get(\"AGENTS_MAILBOX_DIR\", \"\").rstrip(\"/\")\n ) or session_id\n hostname = os.environ.get(\"AGENTS_SYNC_MACHINE_ID\") or socket.gethostname()\n host = re.sub(r\"[^a-z0-9_-]\", \"-\", hostname.split(\".\")[0].strip().lower()) or \"unknown\"\n\n record[\"sessionId\"] = session_id\n record[\"mailboxId\"] = mailbox_id\n record[\"host\"] = host\n record[\"runtime\"] = os.environ.get(\"AGENTS_RUNTIME\", \"headless\")\n cwd = payload.get(\"cwd\") or os.environ.get(\"AGENTS_CWD\")\n if cwd:\n record[\"cwd\"] = cwd\n agent = os.environ.get(\"AGENTS_AGENT_NAME\") or \"claude\"\n record[\"agent\"] = agent\n\n try:\n os.makedirs(activity_dir, exist_ok=True)\n # Cap growth: once over the size limit, keep only milestones.\n if record[\"tier\"] != \"milestone\":\n try:\n if os.path.getsize(target) > MAX_LOG_BYTES:\n return\n except OSError:\n pass\n with open(target, \"a\") as f:\n f.write(json.dumps(record) + \"\\n\")\n except Exception:\n pass # fail open\n\n\nif __name__ == \"__main__\":\n try:\n main()\n except Exception:\n pass # fail open\n";
|
|
114
124
|
/** Hook manifest entries (agents.yaml shape) for the activity-log hook. */
|
|
115
125
|
export declare const ACTIVITY_HOOK_DEFINITIONS: Record<string, Record<string, unknown>>;
|
|
116
126
|
/**
|
package/dist/lib/activity.js
CHANGED
|
@@ -34,6 +34,7 @@ export const MILESTONE_EVENTS = [
|
|
|
34
34
|
'pushed',
|
|
35
35
|
'subagent.spawned',
|
|
36
36
|
'artifact.created',
|
|
37
|
+
'status.posted',
|
|
37
38
|
];
|
|
38
39
|
const MILESTONE_SET = new Set(MILESTONE_EVENTS);
|
|
39
40
|
export function tierForEvent(event) {
|
|
@@ -82,6 +83,10 @@ function parseLine(line) {
|
|
|
82
83
|
tool: parsed.tool,
|
|
83
84
|
detail: parsed.detail,
|
|
84
85
|
url: parsed.url,
|
|
86
|
+
pid: typeof parsed.pid === 'number' ? parsed.pid : undefined,
|
|
87
|
+
launchId: parsed.launchId,
|
|
88
|
+
terminalId: parsed.terminalId,
|
|
89
|
+
tmuxPane: parsed.tmuxPane,
|
|
85
90
|
};
|
|
86
91
|
}
|
|
87
92
|
catch {
|
|
@@ -193,11 +198,11 @@ export function activityEventToRecord(ev) {
|
|
|
193
198
|
hostname: ev.host,
|
|
194
199
|
platform: process.platform,
|
|
195
200
|
arch: process.arch,
|
|
196
|
-
pid: 0,
|
|
201
|
+
pid: ev.pid ?? 0,
|
|
197
202
|
ppid: 0,
|
|
198
203
|
event: ev.event,
|
|
199
204
|
level: 'info',
|
|
200
|
-
caller: 'hook',
|
|
205
|
+
caller: ev.tool === 'feed.post' ? 'agent' : 'hook',
|
|
201
206
|
session: ev.sessionId,
|
|
202
207
|
osUser: ev.agent ?? 'agent',
|
|
203
208
|
transport: 'local',
|
|
@@ -210,6 +215,9 @@ export function activityEventToRecord(ev) {
|
|
|
210
215
|
detail: ev.detail,
|
|
211
216
|
url: ev.url,
|
|
212
217
|
tier: ev.tier,
|
|
218
|
+
...(ev.launchId ? { launchId: ev.launchId } : {}),
|
|
219
|
+
...(ev.terminalId ? { terminalId: ev.terminalId } : {}),
|
|
220
|
+
...(ev.tmuxPane ? { tmuxPane: ev.tmuxPane } : {}),
|
|
213
221
|
};
|
|
214
222
|
}
|
|
215
223
|
/** Read recent activity across sessions as unified {@link EventRecord}s. */
|
|
@@ -230,6 +238,7 @@ export const EVENT_STYLE = {
|
|
|
230
238
|
'pushed': { glyph: '↥', color: chalk.yellow, label: 'pushed' },
|
|
231
239
|
'subagent.spawned': { glyph: '⑂', color: chalk.magenta, label: 'sub-agent spawned' },
|
|
232
240
|
'artifact.created': { glyph: '▤', color: chalk.cyan, label: 'artifact' },
|
|
241
|
+
'status.posted': { glyph: '▸', color: chalk.white, label: 'status' },
|
|
233
242
|
'file.edited': { glyph: '·', color: chalk.gray, label: 'file edited' },
|
|
234
243
|
};
|
|
235
244
|
export function styleForEvent(event) {
|
|
@@ -240,10 +249,13 @@ export function formatActivityLine(ev, opts = {}) {
|
|
|
240
249
|
const s = styleForEvent(ev.event);
|
|
241
250
|
const host = opts.showHost && ev.host && ev.host !== 'unknown' ? chalk.gray(`[${ev.host}] `) : '';
|
|
242
251
|
const label = s.color(`${s.glyph} ${s.label}`);
|
|
243
|
-
|
|
252
|
+
// Status posts are the message; allow a longer snippet than tool-derived detail.
|
|
253
|
+
const detailLimit = ev.event === 'status.posted' ? 100 : 60;
|
|
254
|
+
const detail = ev.detail ? ` ${truncate(ev.detail, detailLimit)}` : '';
|
|
244
255
|
const url = ev.url ? chalk.gray(` ${ev.url}`) : '';
|
|
256
|
+
const agent = ev.event === 'status.posted' && ev.agent ? chalk.gray(` · ${ev.agent}`) : '';
|
|
245
257
|
const when = chalk.gray(relTime(ev.ts).padStart(7));
|
|
246
|
-
return ` ${when} ${host}${label}${detail}${url}`;
|
|
258
|
+
return ` ${when} ${host}${label}${detail}${agent}${url}`;
|
|
247
259
|
}
|
|
248
260
|
// ---------------------------------------------------------------------------
|
|
249
261
|
// Hook installation
|
|
@@ -281,7 +293,7 @@ MAX_LOG_BYTES = 5 * 1024 * 1024 # cap a pathological session's log
|
|
|
281
293
|
MILESTONE_EVENTS = {
|
|
282
294
|
"plan.created", "pr.opened", "pr.merged", "worktree.created",
|
|
283
295
|
"worktree.removed", "commit.created", "pushed", "subagent.spawned",
|
|
284
|
-
"artifact.created",
|
|
296
|
+
"artifact.created", "status.posted",
|
|
285
297
|
}
|
|
286
298
|
|
|
287
299
|
# Deliverable file types + locations -- a Write here is a recognizable artifact
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ActorConfig } from './types.js';
|
|
2
|
+
export type ActorKind = 'human' | 'agent';
|
|
3
|
+
export interface ResolvedActor {
|
|
4
|
+
/**
|
|
5
|
+
* Stable id for the responsible entity: the tailnet login (usually an email)
|
|
6
|
+
* for a resolved human, or `UNRESOLVED@<host>` when it can't be determined.
|
|
7
|
+
*/
|
|
8
|
+
id: string;
|
|
9
|
+
kind: ActorKind;
|
|
10
|
+
/** Human-readable name, for git author + display. */
|
|
11
|
+
name?: string;
|
|
12
|
+
/** Email, for git author + as a durable key. */
|
|
13
|
+
email?: string;
|
|
14
|
+
/** GitHub handle, when the actors map records one. */
|
|
15
|
+
github?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Result of `tailscale whois --json <ip>` we care about. */
|
|
18
|
+
export interface WhoisIdentity {
|
|
19
|
+
login?: string;
|
|
20
|
+
displayName?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Map a resolved tailnet identity (+ the actors map) to a ResolvedActor. Pure --
|
|
24
|
+
* the impure whois/config reads happen in computeActor -- so the enrich/override
|
|
25
|
+
* path is fully testable. No login (local, or an unnameable peer) yields the
|
|
26
|
+
* honest `UNRESOLVED@<host>`; a login without a config entry still credits git
|
|
27
|
+
* from the tailnet DisplayName + login email.
|
|
28
|
+
*/
|
|
29
|
+
export declare function actorFromIdentity(who: WhoisIdentity | undefined, host: string, actors: Record<string, ActorConfig>): ResolvedActor;
|
|
30
|
+
/**
|
|
31
|
+
* Compute the actor for a given environment. Pure with respect to `env` (the
|
|
32
|
+
* only impurity is the `tailscale whois` / config read on the fresh-SSH path),
|
|
33
|
+
* so tests can drive every branch by passing an env explicitly.
|
|
34
|
+
*/
|
|
35
|
+
export declare function computeActor(env?: NodeJS.ProcessEnv): ResolvedActor;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the actor for the current process, cached for the process lifetime
|
|
38
|
+
* (the SSH `whois` shell-out runs at most once).
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveActor(): ResolvedActor;
|
|
41
|
+
/** Clear the per-process cache. For tests, and for env changes within a run. */
|
|
42
|
+
export declare function resetActorCache(): void;
|
|
43
|
+
/**
|
|
44
|
+
* The env an actor propagates to child processes. Always carries the actor id +
|
|
45
|
+
* kind (so children inherit and don't re-resolve). For a resolved human with a
|
|
46
|
+
* real name and email, it also carries `GIT_AUTHOR_*` / `GIT_COMMITTER_*` so the
|
|
47
|
+
* agent's own commits are credited to the person, not the shared account. An
|
|
48
|
+
* unresolved actor sets no git identity -- git keeps its ambient config.
|
|
49
|
+
*/
|
|
50
|
+
export declare function actorEnv(actor: ResolvedActor): Record<string, string>;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actor provenance -- who initiated a run.
|
|
3
|
+
*
|
|
4
|
+
* One shared account means every session, commit, and event otherwise shows up
|
|
5
|
+
* as the same person. `resolveActor()` answers "which human is behind this run"
|
|
6
|
+
* by looking at how the process was reached:
|
|
7
|
+
*
|
|
8
|
+
* - Over SSH (the shared-fleet case): `tailscale whois` the SSH client IP to
|
|
9
|
+
* the connecting tailnet identity -- a real name + login email.
|
|
10
|
+
* - Locally (non-SSH): we can't honestly say who is at the box, so the id is
|
|
11
|
+
* `UNRESOLVED@<host>` and no personal git identity is claimed.
|
|
12
|
+
* - Inherited: a child spawn trusts the `AGENTS_ACTOR*` env its parent
|
|
13
|
+
* stamped rather than re-resolving, so the whole spawn tree shares one actor.
|
|
14
|
+
*
|
|
15
|
+
* The resolved actor rides the child-process env (`actorEnv()`, merged into
|
|
16
|
+
* `buildExecEnv`). When the actor is a resolved human, that env also carries
|
|
17
|
+
* `GIT_AUTHOR_*` / `GIT_COMMITTER_*`, so the agent's own `git commit` is credited
|
|
18
|
+
* to the person instead of the shared account.
|
|
19
|
+
*
|
|
20
|
+
* The optional `actors:` map in agents.yaml enriches or overrides a resolved
|
|
21
|
+
* identity (pin a work email, add a github handle, mark an entry as an agent).
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from 'child_process';
|
|
24
|
+
import { machineId } from './machine-id.js';
|
|
25
|
+
import { parseSshConnection } from './session/provenance.js';
|
|
26
|
+
import { readMeta } from './state.js';
|
|
27
|
+
/** Hard cap on the whois shell-out — it runs on the spawn hot path. */
|
|
28
|
+
const WHOIS_TIMEOUT_MS = 2000;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the tailnet identity behind an IP via `tailscale whois`. Returns
|
|
31
|
+
* undefined when tailscale is absent, the peer is unknown, the call fails, or it
|
|
32
|
+
* exceeds WHOIS_TIMEOUT_MS -- every one of those falls back to an unresolved
|
|
33
|
+
* actor, never an error and never a hang (a wedged tailscaled is timed out, not
|
|
34
|
+
* waited on, since this sits in buildExecEnv on every agent spawn).
|
|
35
|
+
*/
|
|
36
|
+
function tailscaleWhois(ip) {
|
|
37
|
+
try {
|
|
38
|
+
const res = spawnSync('tailscale', ['whois', '--json', ip], {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
windowsHide: true,
|
|
41
|
+
timeout: WHOIS_TIMEOUT_MS,
|
|
42
|
+
});
|
|
43
|
+
// A timeout leaves status null (child killed by SIGTERM) -- the status guard
|
|
44
|
+
// below treats it as an unresolved actor, same as any other failure.
|
|
45
|
+
if (res.status !== 0 || !res.stdout)
|
|
46
|
+
return undefined;
|
|
47
|
+
const data = JSON.parse(res.stdout);
|
|
48
|
+
const up = data.UserProfile;
|
|
49
|
+
if (!up)
|
|
50
|
+
return undefined;
|
|
51
|
+
return { login: up.LoginName, displayName: up.DisplayName };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Read the actors map from config, tolerant of a missing/unreadable config. */
|
|
58
|
+
function readActors() {
|
|
59
|
+
try {
|
|
60
|
+
return readMeta().actors ?? {};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Find the actors-map entry for a resolved tailnet login. Matches on an entry's
|
|
68
|
+
* explicit `login`, its map key, or its `email` (all case-insensitive).
|
|
69
|
+
*/
|
|
70
|
+
function findActorConfig(login, actors) {
|
|
71
|
+
const needle = login.toLowerCase();
|
|
72
|
+
for (const [key, cfg] of Object.entries(actors)) {
|
|
73
|
+
const candidates = [cfg.login, key, cfg.email].filter((v) => !!v);
|
|
74
|
+
if (candidates.some((c) => c.toLowerCase() === needle))
|
|
75
|
+
return cfg;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Map a resolved tailnet identity (+ the actors map) to a ResolvedActor. Pure --
|
|
81
|
+
* the impure whois/config reads happen in computeActor -- so the enrich/override
|
|
82
|
+
* path is fully testable. No login (local, or an unnameable peer) yields the
|
|
83
|
+
* honest `UNRESOLVED@<host>`; a login without a config entry still credits git
|
|
84
|
+
* from the tailnet DisplayName + login email.
|
|
85
|
+
*/
|
|
86
|
+
export function actorFromIdentity(who, host, actors) {
|
|
87
|
+
const login = who?.login;
|
|
88
|
+
if (!login) {
|
|
89
|
+
return { id: `UNRESOLVED@${host}`, kind: 'human' };
|
|
90
|
+
}
|
|
91
|
+
const cfg = findActorConfig(login, actors);
|
|
92
|
+
const emailFromLogin = login.includes('@') ? login : undefined;
|
|
93
|
+
return {
|
|
94
|
+
id: login,
|
|
95
|
+
kind: cfg?.kind ?? 'human',
|
|
96
|
+
name: cfg?.name ?? who?.displayName,
|
|
97
|
+
email: cfg?.email ?? emailFromLogin,
|
|
98
|
+
github: cfg?.github,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** Reconstruct an actor an ancestor process already resolved into the env. */
|
|
102
|
+
function inheritedActor(env) {
|
|
103
|
+
const id = env.AGENTS_ACTOR;
|
|
104
|
+
if (!id)
|
|
105
|
+
return undefined;
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
kind: env.AGENTS_ACTOR_KIND === 'agent' ? 'agent' : 'human',
|
|
109
|
+
name: env.AGENTS_ACTOR_NAME || undefined,
|
|
110
|
+
email: env.AGENTS_ACTOR_EMAIL || undefined,
|
|
111
|
+
github: env.AGENTS_ACTOR_GITHUB || undefined,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Compute the actor for a given environment. Pure with respect to `env` (the
|
|
116
|
+
* only impurity is the `tailscale whois` / config read on the fresh-SSH path),
|
|
117
|
+
* so tests can drive every branch by passing an env explicitly.
|
|
118
|
+
*/
|
|
119
|
+
export function computeActor(env = process.env) {
|
|
120
|
+
const inherited = inheritedActor(env);
|
|
121
|
+
if (inherited)
|
|
122
|
+
return inherited;
|
|
123
|
+
const ssh = env.SSH_CONNECTION ? parseSshConnection(env.SSH_CONNECTION) : undefined;
|
|
124
|
+
const who = ssh?.clientIp ? tailscaleWhois(ssh.clientIp) : undefined;
|
|
125
|
+
return actorFromIdentity(who, machineId(), readActors());
|
|
126
|
+
}
|
|
127
|
+
let cached;
|
|
128
|
+
/**
|
|
129
|
+
* Resolve the actor for the current process, cached for the process lifetime
|
|
130
|
+
* (the SSH `whois` shell-out runs at most once).
|
|
131
|
+
*/
|
|
132
|
+
export function resolveActor() {
|
|
133
|
+
if (!cached)
|
|
134
|
+
cached = computeActor(process.env);
|
|
135
|
+
return cached;
|
|
136
|
+
}
|
|
137
|
+
/** Clear the per-process cache. For tests, and for env changes within a run. */
|
|
138
|
+
export function resetActorCache() {
|
|
139
|
+
cached = undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The env an actor propagates to child processes. Always carries the actor id +
|
|
143
|
+
* kind (so children inherit and don't re-resolve). For a resolved human with a
|
|
144
|
+
* real name and email, it also carries `GIT_AUTHOR_*` / `GIT_COMMITTER_*` so the
|
|
145
|
+
* agent's own commits are credited to the person, not the shared account. An
|
|
146
|
+
* unresolved actor sets no git identity -- git keeps its ambient config.
|
|
147
|
+
*/
|
|
148
|
+
export function actorEnv(actor) {
|
|
149
|
+
const env = {
|
|
150
|
+
AGENTS_ACTOR: actor.id,
|
|
151
|
+
AGENTS_ACTOR_KIND: actor.kind,
|
|
152
|
+
};
|
|
153
|
+
if (actor.name)
|
|
154
|
+
env.AGENTS_ACTOR_NAME = actor.name;
|
|
155
|
+
if (actor.email)
|
|
156
|
+
env.AGENTS_ACTOR_EMAIL = actor.email;
|
|
157
|
+
if (actor.github)
|
|
158
|
+
env.AGENTS_ACTOR_GITHUB = actor.github;
|
|
159
|
+
if (actor.kind === 'human' && actor.name && actor.email) {
|
|
160
|
+
env.GIT_AUTHOR_NAME = actor.name;
|
|
161
|
+
env.GIT_AUTHOR_EMAIL = actor.email;
|
|
162
|
+
env.GIT_COMMITTER_NAME = actor.name;
|
|
163
|
+
env.GIT_COMMITTER_EMAIL = actor.email;
|
|
164
|
+
}
|
|
165
|
+
return env;
|
|
166
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// The production VersionProvider: the only fs/meta-coupled file in the engine.
|
|
2
2
|
// Everything else in this module is pure and testable without it.
|
|
3
|
-
import { listInstalledVersions, getGlobalDefault, getProjectVersion, isVersionInstalled, } from '../versions.js';
|
|
3
|
+
import { listInstalledVersions, getGlobalDefault, getIsolatedDefault, getProjectVersion, isVersionInstalled, } from '../versions.js';
|
|
4
4
|
export const defaultVersionProvider = {
|
|
5
5
|
listInstalled: (agent) => listInstalledVersions(agent),
|
|
6
6
|
getProjectVersion: (agent, cwd) => getProjectVersion(agent, cwd),
|
|
7
7
|
getGlobalDefault: (agent) => getGlobalDefault(agent),
|
|
8
|
+
getIsolatedDefault: (agent) => getIsolatedDefault(agent),
|
|
8
9
|
isInstalled: (agent, version) => isVersionInstalled(agent, version),
|
|
9
10
|
};
|
|
@@ -66,6 +66,15 @@ export function resolveAgentTargets(spec, provider, opts = {}) {
|
|
|
66
66
|
push(agent, glob, 'global-default');
|
|
67
67
|
continue;
|
|
68
68
|
}
|
|
69
|
+
// An isolated-only agent never has a global default — that is the point — so
|
|
70
|
+
// without this step `--agents codex` threw "No default version set" at a user
|
|
71
|
+
// who had explicitly run `agents use codex@<v>`. resolveVersion already had
|
|
72
|
+
// this fallback; the two resolvers had drifted apart.
|
|
73
|
+
const iso = provider.getIsolatedDefault(agent);
|
|
74
|
+
if (iso) {
|
|
75
|
+
push(agent, iso, 'isolated-default');
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
69
78
|
const installed = provider.listInstalled(agent);
|
|
70
79
|
if (installed.length === 0) {
|
|
71
80
|
push(agent, null, 'none');
|
|
@@ -81,13 +90,17 @@ export function resolveAgentTargets(spec, provider, opts = {}) {
|
|
|
81
90
|
}
|
|
82
91
|
throw new AgentSpecError(`No default version set for ${name}. Specify one (${agent}@<version>) or set it: agents use ${agent}@<version>.`, 'no-default', agent, installed);
|
|
83
92
|
}
|
|
84
|
-
// ----- @pinned / @default: the configured global
|
|
93
|
+
// ----- @pinned / @default: the configured default (global, else isolated) -----
|
|
85
94
|
if (qualifier === 'pinned' || qualifier === 'default') {
|
|
86
|
-
const
|
|
95
|
+
const glob = provider.getGlobalDefault(agent);
|
|
96
|
+
// Report which kind it was: an isolated default owns none of the launcher /
|
|
97
|
+
// shim / config-symlink machinery a global default does, and callers that log
|
|
98
|
+
// the source shouldn't claim otherwise.
|
|
99
|
+
const def = glob ?? provider.getIsolatedDefault(agent);
|
|
87
100
|
if (!def) {
|
|
88
101
|
throw new AgentSpecError(`No default version set for ${name}. Run: agents use ${agent}@<version>`, 'no-default', agent, provider.listInstalled(agent));
|
|
89
102
|
}
|
|
90
|
-
push(agent, def, 'global-default(@pinned)');
|
|
103
|
+
push(agent, def, glob ? 'global-default(@pinned)' : 'isolated-default');
|
|
91
104
|
continue;
|
|
92
105
|
}
|
|
93
106
|
// ----- @all: every installed version -----
|
|
@@ -168,7 +181,8 @@ export function resolveListFilter(agent, qualifier, provider, opts = {}) {
|
|
|
168
181
|
const q = qualifier?.trim();
|
|
169
182
|
if (!q || q === 'any')
|
|
170
183
|
return undefined;
|
|
171
|
-
if (q === 'default' || q === 'pinned')
|
|
172
|
-
return provider.getGlobalDefault(agent) ?? undefined;
|
|
184
|
+
if (q === 'default' || q === 'pinned') {
|
|
185
|
+
return provider.getGlobalDefault(agent) ?? provider.getIsolatedDefault(agent) ?? undefined;
|
|
186
|
+
}
|
|
173
187
|
return resolveSingleAgentTarget(`${agent}@${q}`, provider, { ...opts, availableAgents: [agent] }).version;
|
|
174
188
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentId } from '../types.js';
|
|
2
2
|
/** How a resolved version was arrived at — for provenance, display, and error rendering. */
|
|
3
|
-
export type VersionSource = 'explicit' | 'project-pin' | 'global-default' | 'global-default(@pinned)' | 'sole-installed' | 'newest-installed' | 'alias-latest' | 'alias-oldest' | 'none';
|
|
3
|
+
export type VersionSource = 'explicit' | 'project-pin' | 'global-default' | 'isolated-default' | 'global-default(@pinned)' | 'sole-installed' | 'newest-installed' | 'alias-latest' | 'alias-oldest' | 'none';
|
|
4
4
|
export interface AgentTarget {
|
|
5
5
|
agent: AgentId;
|
|
6
6
|
/** Resolved exact version, or null when the agent has no installed versions yet. */
|
|
@@ -32,6 +32,14 @@ export interface VersionProvider {
|
|
|
32
32
|
getProjectVersion(agent: AgentId, cwd: string): string | null;
|
|
33
33
|
/** The configured global default version, or null. */
|
|
34
34
|
getGlobalDefault(agent: AgentId): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* The preferred isolated copy. Kept separate from getGlobalDefault on purpose: a
|
|
37
|
+
* global default owns the launcher, the bare shim and the real ~/.<agent> config
|
|
38
|
+
* symlink, and an isolated version must never acquire any of those. Folding the
|
|
39
|
+
* two together here would hand an isolated version back to every caller that
|
|
40
|
+
* reasonably assumes a global default means "the one that owns the launcher".
|
|
41
|
+
*/
|
|
42
|
+
getIsolatedDefault(agent: AgentId): string | null;
|
|
35
43
|
/** Whether an exact version is installed. */
|
|
36
44
|
isInstalled(agent: AgentId, version: string): boolean;
|
|
37
45
|
}
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -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,51 @@ 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;
|
|
268
|
+
/**
|
|
269
|
+
* Whether a Claude version home holds its OWN on-disk credential — a non-blank
|
|
270
|
+
* `.claude/.credentials.json` (the store Claude Code writes on login off macOS)
|
|
271
|
+
* or a `.claude/.oauth_token` file (the keychain-less shim fallback, shims.ts).
|
|
272
|
+
*
|
|
273
|
+
* RUSH-1979: the routines daemon injects one ambient `CLAUDE_CODE_OAUTH_TOKEN`
|
|
274
|
+
* (RUSH-1759) so a token-less default account still authenticates. But Claude —
|
|
275
|
+
* and the Linux shim's own `-z CLAUDE_CODE_OAUTH_TOKEN` guard — both prefer that
|
|
276
|
+
* env var over a pinned account's config dir, so when balanced rotation pins a
|
|
277
|
+
* specific account's `CLAUDE_CONFIG_DIR` the ambient token shadows it: the pool
|
|
278
|
+
* is inert and every fire 401s on the one stale token. A routine spawn drops the
|
|
279
|
+
* ambient token when the pinned account has its own credential (this returns
|
|
280
|
+
* true) so the account authenticates itself; when it does not (the RUSH-1759
|
|
281
|
+
* default), the injected token is kept.
|
|
282
|
+
*
|
|
283
|
+
* Off-darwin only, mirroring {@link isClaudeCredentialFileBlank}: on macOS the
|
|
284
|
+
* login Keychain is the canonical store and probing it raises an auth sheet, and
|
|
285
|
+
* there the daemon injects no token to shadow (broker-only, usually absent), so
|
|
286
|
+
* returning false changes nothing. Sync, no Keychain, no network.
|
|
287
|
+
*/
|
|
288
|
+
export declare function claudeHomeHasOwnCredential(base: string, platform?: NodeJS.Platform): boolean;
|
|
233
289
|
export declare function getAccountInfo(agentId: AgentId, home?: string): Promise<AccountInfo>;
|
|
234
290
|
/**
|
|
235
291
|
* Determine when the agent was last used by checking session file mtimes,
|