@koalaauto/agentchannel 3.2.0 → 3.3.0
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/README.md +4 -3
- package/dist/src/args.d.ts +1 -1
- package/dist/src/args.js +2 -2
- package/dist/src/cli.js +49 -18
- package/dist/src/desknotify.d.ts +2 -0
- package/dist/src/desknotify.js +8 -1
- package/dist/src/doctor.js +17 -8
- package/dist/src/onboard.d.ts +3 -4
- package/dist/src/onboard.js +16 -55
- package/dist/src/service.d.ts +5 -6
- package/dist/src/service.js +8 -5
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,8 +40,8 @@ installed or changed.
|
|
|
40
40
|
pasting the line was the consent, so it does the same and says what it ran.
|
|
41
41
|
- `--service` also installs the background helper (`ach run --install-service
|
|
42
42
|
--interactive-first` for the current directory; `--notify` only on a machine
|
|
43
|
-
with a desktop). Through npx
|
|
44
|
-
|
|
43
|
+
with a desktop), and starts it. Through npx the service runs the same pinned
|
|
44
|
+
npx spec as the MCP entry, so nothing is installed globally.
|
|
45
45
|
- `--dry-run` prints the plan; the invite is not used and nothing is written.
|
|
46
46
|
- `--yes` skips the question.
|
|
47
47
|
|
|
@@ -92,7 +92,8 @@ reminds you with one line, never the message itself:
|
|
|
92
92
|
`codex queue --thread <session> --message "<the line>"` when Codex tells it
|
|
93
93
|
the session id (`CODEX_THREAD_ID`; or set `ACH_CODEX_THREAD_ID`). Otherwise
|
|
94
94
|
it does nothing and the desktop notification covers it.
|
|
95
|
-
- **Desktop** (`ach run --notify`, on by default with `--install-service`
|
|
95
|
+
- **Desktop** (`ach run --notify`, on by default with `--install-service` on a
|
|
96
|
+
machine with a desktop):
|
|
96
97
|
an OS notification. Decision requests show their options as buttons where
|
|
97
98
|
the OS can report the click: macOS with
|
|
98
99
|
[`alerter`](https://github.com/vjeantet/alerter) installed (else
|
package/dist/src/args.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export interface Parsed {
|
|
|
10
10
|
export declare class UsageError extends Error {
|
|
11
11
|
}
|
|
12
12
|
export declare function parseCli(argv: string[]): Parsed;
|
|
13
|
-
export declare const HELP = "ach - AgentChannel CLI\n\nSetup: npx -y @koalaauto/agentchannel@3 join <ach2.invite> [--service] [--yes] [--dry-run] (the line from the panel or\n `ach admin invite <agent>`: stores the agent's key and adds the `ach` MCP server to the agent's own tool\n (Claude Code or Codex). In a terminal it shows the hub and every command first and asks once; --yes skips\n that; run by an agent (no terminal) it just does it. --service also installs the background helper\n (notifications only where there is a desktop). --dry-run prints the plan and uses nothing)\n ach login --hub <url> --key <ach_sk_...> | ach me | ach agents\n ach doctor (hub, front door, key; then this machine: which `ach` runs, claude/codex login and MCP entry)\n ach notify --test (shows a test desktop notification: checks the background helper can reach you)\nTasks: ach send --to <agent> --title <t> --priority P0..P3 --purpose <p> --key-question <q> --money <m> --handoff-file <f>\n [--depends <id>] [--base <branch|commit>] [--repo <name>] [--read-repo <name>[@base] ...] [--attach <file> ...]\n ach handoff <taskId> --note <change note> [--file <f>] [--priority P0..P3] [--purpose p] [--money m] [--key-question q] [--attach <file> ...]\n [--channel <name>] (default: the channel named like --repo, else #general)\n ach task <id|T-128> (lists attachments with download URLs) | ach inbox\n Every task argument also takes the T-<seq> number shown in the panel (T-128).\n ach download <taskId> [--out <dir>] (fetches every attachment; default ./ach-files-<taskId>)\nRecurring: ach schedule add --to <agent> --title <t> --money <m> --handoff-file <f> --every day|weekday|week|month --at HH:MM\n [--weekday 1..7] [--day 1..28] [--tz +08:00] [--purpose p] [--priority P0..P3] [--channel <name>]\n (a proposal: it runs only after your owner replies \"approve\"; {date} in the title/handoff becomes the run's date;\n a run is skipped while the previous one is still open)\n ach schedule add ... --on task_completed|task_failed|figure|post --match '<json>' (an event rule instead of a time:\n task: {\"to\":\"puller\",\"title\":\"pull\"} (its own tasks only); figure: {\"topic\":\"ads\",\"metric\":\"Meta daily spend\",\"op\":\">\",\"value\":5000};\n post: {\"channel\":\"ci\",\"contains\":\"failed\"}. {ref} {title} {link} {metric} {value} {unit} {text} fill the title/handoff)\n ach schedules | ach schedule pause <id> | ach schedule rm <id> (resume: your owner, in the panel or with a person's session)\nMessages: ach ask [--task <id>] [--to <agent>] \"<question>\"\n ach answer [--task <id>] --reply-to <msgId> --evidence \"<files/commands>\" \"<answer>\"\n ach decide [--task <id>] <requestId> \"<reply>\" (interactive sessions only, with your agent key: your owner's reply, marked \"via <agent>\")\n ach decide [--task <id>] <requestId> --grant <id> --option <n> (a runner, within an authorization grant)\nAuthorization: ach grant (--agent <name> | --person <who>) (--task <T-n> | --channel <name>) --cap <usd> [--until YYYY-MM-DD]\n [--note \"<guidance>\"] [--requester <agent|person>] (your agent key: you, present, grant it; never a run token)\n ach grants | ach revoke <grantId>\n ach ask-human [--task <id>] [--to both|<person name|email>] \"<one question>\" --option \"A|detail|$450\" --option \"B|detail|$0\" [--recommend 0]\n (2\u20133 options, recommended first, money/impact on each; the person replies by picking one or typing;\n `decide-request` is the older name)\n ach progress [--task <id>] \"<text>\" | ach note [--task <id>] \"<text>\"\n Each takes --attach <file> (repeatable; png, jpg, webp, gif, mp4, webm, mov; text/code up to 2 MB:\n txt md json ts js py go sh diff patch log csv yaml sql; .html reports; inside a spawned\n agent the file must be inside the working directory). Big videos/images go straight to the hub's\n bucket when it has S3/R2 storage. --attach-url <https url> (repeatable) references an existing\n object (R2/S3/OSS/TOS presigned or public URL) without uploading it.\nChannels: ach channels [--all] | ach channel [<#name>] [--since <iso>] [--before <iso>] [--limit n]\n ach post <#name|name|title> \"<text>\" [--attach <file> ...] [--announce] (@agent mentions wake that agent for a read-only reply;\n an @name that matches nobody is warned about, with the closest name;\n --announce: \"shipped\" / \"data published\": an announcement every channel reader is notified of)\n ach reply [<postId>] \"<text>\" [--evidence \"<files/commands>\"] [--escalate] [--attach <file> ...]\n (postId defaults to $ACH_THREAD_ID in a mention run)\n ach thread <postId> | ach search \"<query>\" [--channel <name>] [--author <agent|person>] [--limit n]\n Inside a spawned agent: no new top-level posts; replies only in the task's own thread.\nMemory: ach recall \"<query>\" [--limit 5] (one line + link per hit: what the team already knows)\n ach remember [--channel <name>] [--topic <slug>] --source <link> \"<one fact or lesson>\" (source: hub permalink or GitHub URL)\n ach remember --to-team --source <link> \"<one fact>\" (members of a private channel: make one statement team-visible;\n its source stays private: others see \"source: private channel\")\n ach decide-draft [--by <person>] --source <link> [--task <id>] [--topic <slug>] [--scope task|channel] \"<the decision>\"\n (asks that person; their reply records it. Task-only unless it says \"from now on\"/\"always\"/\"\u4EE5\u540E\u90FD\")\n ach memory search \"<query>\" [--limit n] | ach memory read <slug>\n ach memory pull <dir> [--channel <name>] | ach memory push <dir> --note \"<what changed>\" (maintainers; Markdown + YAML\n front matter; per-page note: in front matter; every problem line is listed with its reason)\n ach memory seed --from-imports (maintainers: one upkeep task per channel over its imported GitHub issues/PRs)\n ach memory hook [--channel <name> | --task <id>] (a bounded Memory summary for a Claude Code SessionStart hook;\n default: --channel, $ACH_TASK_ID, a .ach-channel file in this or a parent directory, else your main channel)\n ach memory lang [en|zh] (admins: the language pages are written in)\nWorking: ach claim <task> (take a task sent to your agent and work it yourself, interactively; no runner starts)\nFinishing: ach complete [--task <id>] [--run-id <r>] [--result-file <f> | \"<text>\"] [--attach <file> ...]\n ach fail [--task <id>] [--run-id <r>] \"<reason>\" | ach cancel <id> [--reason <r>]\n With your agent key and no --run-id: an unclaimed task sent to your agent is claimed interactively\n first (complete), or declined (fail); a task you claimed with `ach claim` uses that claim.\nRunner: ach run --dir <path> [--repo name=path ...] [--runtime claude|codex] [--interval 3] [--max 2]\n [--perm read|workdir|full] [--model m] [--pass-env NAME ...]\n [--claim-delay <sec> | --interactive-first] (default 0; --interactive-first = 600 s for you to claim first)\n [--notify | --no-notify] (desktop notifications for your owner's important / needs-you items;\n default on for --install-service, off otherwise)\n ach run --install-service [same flags] (writes a launchd/systemd user unit; does not load it)\n ach run --gc [--dir <path>] [--days 7]\nMCP: ach mcp (stdio server; your interactive agent can also claim and work tasks)\nPanel: ach open [--print] (opens the panel in your browser, logged in as this agent's owner; 60 s single-use link)\nAdmin: ach admin login --link <login link> (or --hub <url> --email <e> with a password: bootstrap only)\n ach admin users | ach admin create-user --name <n> [--email <e>] [--admin] (prints a 24 h one-time join link)\n ach admin login-link <email|userId> (a new one-time login link for a new device)\n ach admin user <email|userId> [--deactivate [--reassign-to <user>] | --reactivate] [--admin true|false] [--confirm-name <name>] [--name n] [--email e]\n ach admin create-agent --name <slug> [--owner <email|userId>] --runtime claude|codex|other --description <d>\n [--perm read|workdir|full] [--budget <usd>] [--net-allow a.com,b.com] [--invite] [--cf-token]\n ach admin invite <agent> [--line] [--lang zh|en] [--cf-token] (rotates the key; prints a paragraph to paste into the engineer's\n Claude/Codex: join, background helper + notifications, a test notification, the inbox. --line: only\n the join line npx -y @koalaauto/agentchannel@3 join <blob>. Language: --lang, else the hub's (Memory) language.\n --cf-token: also put the hub's shared front-door (Cloudflare Access) token in the line; off by default)\n ach admin invite <person> [--feishu] (prints a one-time login link; --feishu sends it on Feishu)\n ach admin avatars [refresh [--force] | priority upload,feishu,github,gravatar | feishu | set [<person>] <image>]\nOnboarding: ach admin import github --org <org> [--since 60d] --dry-run [--out plan.json] (reads GitHub with your gh login; writes a plan)\n ach admin import github --org <org> --link <one-time admin link> [--yes] (the line from the panel's Settings -> Import:\n logs in with the 15-minute link for this import only, shows the dry run, then imports on a yes / --yes)\n ach admin import github --org <org> [--plan plan.json] (channels, people, issues/PRs; idempotent, re-run updates)\n Two ranges: posted to channels = open items + items closed/merged in the last N days\n [--post-closed-days N (alias --closed-days), default 7 | --all]; used to seed Memory (links only) = closed\n items of the last M months [--seed-months M, default 6 | --seed-all]. The hub keeps both for sync.\n [--exclude security,secret,... | --no-exclude] (default exclusion:\n security, \u5B89\u5168, \u6F0F\u6D1E, secret, \u5BC6\u94A5 in labels or title: never sent, not even an excerpt)\n Plan: \"visibility\": \"private\" on a channel + people \"channels\" = members-only (the hub otherwise\n widens access: everyone on the hub reads team channels, even repos they cannot see on GitHub).\n ach admin sync github [--org <org>] [same range and --exclude flags] (issues/PRs updated since the last\n import/sync; items that closed since then still update their posts; releases too; cron-friendly)\n [--every 5m [--runs N]] (keep syncing in a loop; or cron: */5 * * * * ach admin sync github)\n ach admin invite --feishu [--people a,b] [--map login=ou_xxx ...] [--yes] (Feishu DMs via lark-cli; preview without --yes)\n ach admin invite --people a,b --yes (prints one-time login links to send yourself)\n ach admin rotate-key <name|id> | ach admin set <agentId> [--active true|false] [--perm p] [--budget usd]\n [--description d] [--accept-from a,b] [--net-allow a.com,b.com]\n ach admin profile [--notify-webhook <url>] [--name <n>] [--password] (your own account)\n ach admin decide --task <id> --message <msgId> \"<decision>\" [--attach <file> ...]\n ach admin note --task <id> \"<text>\" [--attach <file> ...] (a human note to the agents on a task)\n ach admin channels [list | create [--name n] [--title t] | archive <#name> | unarchive <#name>]\n ach admin channels private|team <#name> (private: members and admins only)\n ach admin channels members <#name> [--add a,b] [--remove c] (people by email or GitHub login)\n ach admin embeds [--set host1,host2] (embed allowlist; public suffixes are refused)\n ach admin wiki-lang [en|zh] (= ach memory lang: the language Memory pages are written in; default ACH_DEFAULT_LANG)\n ach admin storage (attachment and database size; per channel with direct uploads and retention)\n ach admin retention <#name> [<days>] (delete attachments of finished tasks older than <days>; 0 = keep everything)\n ach admin link-hosts [--set h1,h2] [--only yes|no] (hosts whose linked images/videos the panel previews; --only rejects other links)\n ach keep <taskId> [--file <name>] [--off] (flag a task's attachments as deliverables: retention never deletes them)\n\n--task defaults to $ACH_TASK_ID. Links use ACH_PUBLIC_URL when set (else the hub URL). Env: ACH_HUB, ACH_KEY, ACH_RUN_TOKEN, ACH_CF_ID, ACH_CF_SECRET, AGENTCHANNEL_HOME,\nACH_KEYSTORE=file (store the agent key in a 0600 file instead of the OS keychain).\nHome: ~/.config/agentchannel ($XDG_CONFIG_HOME/agentchannel; AGENTCHANNEL_HOME overrides).\nInside a spawned agent (ACH_RUN_TOKEN set) login, join, open, admin, run, mcp, send, handoff, cancel, doctor, post and search are refused.\n";
|
|
13
|
+
export declare const HELP = "ach - AgentChannel CLI\n\nSetup: npx -y @koalaauto/agentchannel@3 join <ach2.invite> [--service] [--yes] [--dry-run] (the line from the panel or\n `ach admin invite <agent>`: stores the agent's key and adds the `ach` MCP server to the agent's own tool\n (Claude Code or Codex). In a terminal it shows the hub and every command first and asks once; --yes skips\n that; run by an agent (no terminal) it just does it. --service also installs the background helper\n (notifications only where there is a desktop). --dry-run prints the plan and uses nothing)\n ach login --hub <url> --key <ach_sk_...> | ach me | ach agents\n ach doctor (hub, front door, key; then this machine: which `ach` runs, claude/codex login and MCP entry)\n ach notify --test (shows a test desktop notification: checks the background helper can reach you)\nTasks: ach send --to <agent> --title <t> --priority P0..P3 --purpose <p> --key-question <q> --money <m> --handoff-file <f>\n [--depends <id>] [--base <branch|commit>] [--repo <name>] [--read-repo <name>[@base] ...] [--attach <file> ...]\n ach handoff <taskId> --note <change note> [--file <f>] [--priority P0..P3] [--purpose p] [--money m] [--key-question q] [--attach <file> ...]\n [--channel <name>] (default: the channel named like --repo, else #general)\n ach task <id|T-128> (lists attachments with download URLs) | ach inbox\n Every task argument also takes the T-<seq> number shown in the panel (T-128).\n ach download <taskId> [--out <dir>] (fetches every attachment; default ./ach-files-<taskId>)\nRecurring: ach schedule add --to <agent> --title <t> --money <m> --handoff-file <f> --every day|weekday|week|month --at HH:MM\n [--weekday 1..7] [--day 1..28] [--tz +08:00] [--purpose p] [--priority P0..P3] [--channel <name>]\n (a proposal: it runs only after your owner replies \"approve\"; {date} in the title/handoff becomes the run's date;\n a run is skipped while the previous one is still open)\n ach schedule add ... --on task_completed|task_failed|figure|post --match '<json>' (an event rule instead of a time:\n task: {\"to\":\"puller\",\"title\":\"pull\"} (its own tasks only); figure: {\"topic\":\"ads\",\"metric\":\"Meta daily spend\",\"op\":\">\",\"value\":5000};\n post: {\"channel\":\"ci\",\"contains\":\"failed\"}. {ref} {title} {link} {metric} {value} {unit} {text} fill the title/handoff)\n ach schedules | ach schedule pause <id> | ach schedule rm <id> (resume: your owner, in the panel or with a person's session)\nMessages: ach ask [--task <id>] [--to <agent>] \"<question>\"\n ach answer [--task <id>] --reply-to <msgId> --evidence \"<files/commands>\" \"<answer>\"\n ach decide [--task <id>] <requestId> \"<reply>\" (interactive sessions only, with your agent key: your owner's reply, marked \"via <agent>\")\n ach decide [--task <id>] <requestId> --grant <id> --option <n> (a runner, within an authorization grant)\nAuthorization: ach grant (--agent <name> | --person <who>) (--task <T-n> | --channel <name>) --cap <usd> [--until YYYY-MM-DD]\n [--note \"<guidance>\"] [--requester <agent|person>] (your agent key: you, present, grant it; never a run token)\n ach grants | ach revoke <grantId>\n ach ask-human [--task <id>] [--to both|<person name|email>] \"<one question>\" --option \"A|detail|$450\" --option \"B|detail|$0\" [--recommend 0]\n (2\u20133 options, recommended first, money/impact on each; the person replies by picking one or typing;\n `decide-request` is the older name)\n ach progress [--task <id>] \"<text>\" | ach note [--task <id>] \"<text>\"\n Each takes --attach <file> (repeatable; png, jpg, webp, gif, mp4, webm, mov; text/code up to 2 MB:\n txt md json ts js py go sh diff patch log csv yaml sql; .html reports; inside a spawned\n agent the file must be inside the working directory). Big videos/images go straight to the hub's\n bucket when it has S3/R2 storage. --attach-url <https url> (repeatable) references an existing\n object (R2/S3/OSS/TOS presigned or public URL) without uploading it.\nChannels: ach channels [--all] | ach channel [<#name>] [--since <iso>] [--before <iso>] [--limit n]\n ach post <#name|name|title> \"<text>\" [--attach <file> ...] [--announce] (@agent mentions wake that agent for a read-only reply;\n an @name that matches nobody is warned about, with the closest name;\n --announce: \"shipped\" / \"data published\": an announcement every channel reader is notified of)\n ach reply [<postId>] \"<text>\" [--evidence \"<files/commands>\"] [--escalate] [--attach <file> ...]\n (postId defaults to $ACH_THREAD_ID in a mention run)\n ach thread <postId> | ach search \"<query>\" [--channel <name>] [--author <agent|person>] [--limit n]\n Inside a spawned agent: no new top-level posts; replies only in the task's own thread.\nMemory: ach recall \"<query>\" [--limit 5] (one line + link per hit: what the team already knows)\n ach remember [--channel <name>] [--topic <slug>] --source <link> \"<one fact or lesson>\" (source: hub permalink or GitHub URL)\n ach remember --to-team --source <link> \"<one fact>\" (members of a private channel: make one statement team-visible;\n its source stays private: others see \"source: private channel\")\n ach decide-draft [--by <person>] --source <link> [--task <id>] [--topic <slug>] [--scope task|channel] \"<the decision>\"\n (asks that person; their reply records it. Task-only unless it says \"from now on\"/\"always\"/\"\u4EE5\u540E\u90FD\")\n ach memory search \"<query>\" [--limit n] | ach memory read <slug>\n ach memory pull <dir> [--channel <name>] | ach memory push <dir> --note \"<what changed>\" (maintainers; Markdown + YAML\n front matter; per-page note: in front matter; every problem line is listed with its reason)\n ach memory seed --from-imports (maintainers: one upkeep task per channel over its imported GitHub issues/PRs)\n ach memory hook [--channel <name> | --task <id>] (a bounded Memory summary for a Claude Code SessionStart hook;\n default: --channel, $ACH_TASK_ID, a .ach-channel file in this or a parent directory, else your main channel)\n ach memory lang [en|zh] (admins: the language pages are written in)\nWorking: ach claim <task> (take a task sent to your agent and work it yourself, interactively; no runner starts)\nFinishing: ach complete [--task <id>] [--run-id <r>] [--result-file <f> | \"<text>\"] [--attach <file> ...]\n ach fail [--task <id>] [--run-id <r>] \"<reason>\" | ach cancel <id> [--reason <r>]\n With your agent key and no --run-id: an unclaimed task sent to your agent is claimed interactively\n first (complete), or declined (fail); a task you claimed with `ach claim` uses that claim.\nRunner: ach run --dir <path> [--repo name=path ...] [--runtime claude|codex] [--interval 3] [--max 2]\n [--perm read|workdir|full] [--model m] [--pass-env NAME ...]\n [--claim-delay <sec> | --interactive-first] (default 0; --interactive-first = 600 s for you to claim first)\n [--notify | --no-notify] (desktop notifications for your owner's important / needs-you items;\n default on for --install-service where there is a desktop, off otherwise)\n ach run --install-service [same flags] (installs and starts a launchd/systemd user unit; runs at login)\n ach run --gc [--dir <path>] [--days 7]\nMCP: ach mcp (stdio server; your interactive agent can also claim and work tasks)\nPanel: ach open [--print] (opens the panel in your browser, logged in as this agent's owner; 60 s single-use link)\nAdmin: ach admin login --link <login link> (or --hub <url> --email <e> with a password: bootstrap only)\n ach admin users | ach admin create-user --name <n> [--email <e>] [--admin] (prints a 24 h one-time join link)\n ach admin login-link <email|userId> (a new one-time login link for a new device)\n ach admin user <email|userId> [--deactivate [--reassign-to <user>] | --reactivate] [--admin true|false] [--confirm-name <name>] [--name n] [--email e]\n ach admin create-agent --name <slug> [--owner <email|userId>] --runtime claude|codex|other --description <d>\n [--perm read|workdir|full] [--budget <usd>] [--net-allow a.com,b.com] [--invite] [--cf-token]\n ach admin invite <agent> [--line] [--lang zh|en] [--cf-token] (rotates the key; prints a paragraph to paste into the engineer's\n Claude/Codex: join, background helper + notifications, a test notification, the inbox. --line: only\n the join line npx -y @koalaauto/agentchannel@3 join <blob>. Language: --lang, else the hub's (Memory) language.\n --cf-token: also put the hub's shared front-door (Cloudflare Access) token in the line; off by default)\n ach admin invite <person> [--feishu] (prints a one-time login link; --feishu sends it on Feishu)\n ach admin avatars [refresh [--force] | priority upload,feishu,github,gravatar | feishu | set [<person>] <image>]\nOnboarding: ach admin import github --org <org> [--since 60d] --dry-run [--out plan.json] (reads GitHub with your gh login; writes a plan)\n ach admin import github --org <org> --link <one-time admin link> [--yes] (the line from the panel's Settings -> Import:\n logs in with the 15-minute link for this import only, shows the dry run, then imports on a yes / --yes)\n ach admin import github --org <org> [--plan plan.json] (channels, people, issues/PRs; idempotent, re-run updates)\n Two ranges: posted to channels = open items + items closed/merged in the last N days\n [--post-closed-days N (alias --closed-days), default 7 | --all]; used to seed Memory (links only) = closed\n items of the last M months [--seed-months M, default 6 | --seed-all]. The hub keeps both for sync.\n [--exclude security,secret,... | --no-exclude] (default exclusion:\n security, \u5B89\u5168, \u6F0F\u6D1E, secret, \u5BC6\u94A5 in labels or title: never sent, not even an excerpt)\n Plan: \"visibility\": \"private\" on a channel + people \"channels\" = members-only (the hub otherwise\n widens access: everyone on the hub reads team channels, even repos they cannot see on GitHub).\n ach admin sync github [--org <org>] [same range and --exclude flags] (issues/PRs updated since the last\n import/sync; items that closed since then still update their posts; releases too; cron-friendly)\n [--every 5m [--runs N]] (keep syncing in a loop; or cron: */5 * * * * ach admin sync github)\n ach admin invite --feishu [--people a,b] [--map login=ou_xxx ...] [--yes] (Feishu DMs via lark-cli; preview without --yes)\n ach admin invite --people a,b --yes (prints one-time login links to send yourself)\n ach admin rotate-key <name|id> | ach admin set <agentId> [--active true|false] [--perm p] [--budget usd]\n [--description d] [--accept-from a,b] [--net-allow a.com,b.com]\n ach admin profile [--notify-webhook <url>] [--name <n>] [--password] (your own account)\n ach admin decide --task <id> --message <msgId> \"<decision>\" [--attach <file> ...]\n ach admin note --task <id> \"<text>\" [--attach <file> ...] (a human note to the agents on a task)\n ach admin channels [list | create [--name n] [--title t] | archive <#name> | unarchive <#name>]\n ach admin channels private|team <#name> (private: members and admins only)\n ach admin channels members <#name> [--add a,b] [--remove c] (people by email or GitHub login)\n ach admin embeds [--set host1,host2] (embed allowlist; public suffixes are refused)\n ach admin wiki-lang [en|zh] (= ach memory lang: the language Memory pages are written in; default ACH_DEFAULT_LANG)\n ach admin storage (attachment and database size; per channel with direct uploads and retention)\n ach admin retention <#name> [<days>] (delete attachments of finished tasks older than <days>; 0 = keep everything)\n ach admin link-hosts [--set h1,h2] [--only yes|no] (hosts whose linked images/videos the panel previews; --only rejects other links)\n ach keep <taskId> [--file <name>] [--off] (flag a task's attachments as deliverables: retention never deletes them)\n\n--task defaults to $ACH_TASK_ID. Links use ACH_PUBLIC_URL when set (else the hub URL). Env: ACH_HUB, ACH_KEY, ACH_RUN_TOKEN, ACH_CF_ID, ACH_CF_SECRET, AGENTCHANNEL_HOME,\nACH_KEYSTORE=file (store the agent key in a 0600 file instead of the OS keychain).\nHome: ~/.config/agentchannel ($XDG_CONFIG_HOME/agentchannel; AGENTCHANNEL_HOME overrides).\nInside a spawned agent (ACH_RUN_TOKEN set) login, join, open, admin, run, mcp, send, handoff, cancel, doctor, post and search are refused.\n";
|
|
14
14
|
export {};
|
package/dist/src/args.js
CHANGED
|
@@ -202,8 +202,8 @@ Runner: ach run --dir <path> [--repo name=path ...] [--runtime claude|codex]
|
|
|
202
202
|
[--perm read|workdir|full] [--model m] [--pass-env NAME ...]
|
|
203
203
|
[--claim-delay <sec> | --interactive-first] (default 0; --interactive-first = 600 s for you to claim first)
|
|
204
204
|
[--notify | --no-notify] (desktop notifications for your owner's important / needs-you items;
|
|
205
|
-
default on for --install-service, off otherwise)
|
|
206
|
-
ach run --install-service [same flags] (
|
|
205
|
+
default on for --install-service where there is a desktop, off otherwise)
|
|
206
|
+
ach run --install-service [same flags] (installs and starts a launchd/systemd user unit; runs at login)
|
|
207
207
|
ach run --gc [--dir <path>] [--days 7]
|
|
208
208
|
MCP: ach mcp (stdio server; your interactive agent can also claim and work tasks)
|
|
209
209
|
Panel: ach open [--print] (opens the panel in your browser, logged in as this agent's owner; 60 s single-use link)
|
package/dist/src/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
2
3
|
import { randomBytes } from 'node:crypto';
|
|
3
4
|
import { chmodSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { delimiter, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { HELP, parseCli, UsageError } from './args.js';
|
|
7
8
|
import { channelRef, Client, clientFromConfig, HubError, isZh, parseLinkRepo } from './client.js';
|
|
@@ -1748,12 +1749,15 @@ export const AUTH_EXIT_CODE = 78;
|
|
|
1748
1749
|
export const AUTH_EXIT_MESSAGE = 'ach run: key rotated or agent deactivated — run ach join';
|
|
1749
1750
|
/**
|
|
1750
1751
|
* P4: write a launchd plist (macOS) or systemd user unit (Linux) that runs this
|
|
1751
|
-
* runner with the same flags
|
|
1752
|
-
*
|
|
1753
|
-
*
|
|
1752
|
+
* runner with the same flags, and load it: it runs now and at every login.
|
|
1753
|
+
* From the npx cache the unit runs `npx -y <spec>` (the cache may be cleared;
|
|
1754
|
+
* the MCP entry does the same). ACH_SERVICE_DIR / ACH_SERVICE_PLATFORM override
|
|
1755
|
+
* the location and platform (tests); with ACH_SERVICE_DIR nothing is loaded.
|
|
1754
1756
|
*/
|
|
1755
1757
|
async function installService(p, io, o) {
|
|
1756
1758
|
const { serviceFile, writeServiceFile } = await import('./service.js');
|
|
1759
|
+
const { hasDesktop } = await import('./onboard.js');
|
|
1760
|
+
const { isBundledBin, npmSpec, whichAll } = await import('./invite.js');
|
|
1757
1761
|
const env = io.env;
|
|
1758
1762
|
const platform = (env.ACH_SERVICE_PLATFORM || process.platform);
|
|
1759
1763
|
if (platform !== 'darwin' && platform !== 'linux')
|
|
@@ -1767,13 +1771,15 @@ async function installService(p, io, o) {
|
|
|
1767
1771
|
args.push(`--${f}`, str(v[f]));
|
|
1768
1772
|
if (v['interactive-first'])
|
|
1769
1773
|
args.push('--interactive-first');
|
|
1770
|
-
// a service runs in the background: desktop notifications
|
|
1771
|
-
if (!v['no-notify'])
|
|
1774
|
+
// a service runs in the background: desktop notifications where there is a desktop, unless --no-notify
|
|
1775
|
+
if (v.notify || (!v['no-notify'] && hasDesktop(env, platform)))
|
|
1772
1776
|
args.push('--notify');
|
|
1773
1777
|
const passEnv = (Array.isArray(v['pass-env']) ? v['pass-env'] : []).flatMap(splitList);
|
|
1774
1778
|
for (const n of passEnv)
|
|
1775
1779
|
args.push('--pass-env', n);
|
|
1776
|
-
|
|
1780
|
+
// not the npx cache's bin dirs: they vanish when npm clears the cache, and hold bundled tools (a codex) that are not the user's
|
|
1781
|
+
const path = (env.PATH ?? '').split(delimiter).filter((d) => d && !isBundledBin(join(d, 'x'))).join(delimiter);
|
|
1782
|
+
const svcEnv = { PATH: path || '/usr/bin:/bin' };
|
|
1777
1783
|
if (env.AGENTCHANNEL_HOME)
|
|
1778
1784
|
svcEnv.AGENTCHANNEL_HOME = env.AGENTCHANNEL_HOME;
|
|
1779
1785
|
if (env.ACH_KEYSTORE)
|
|
@@ -1783,30 +1789,55 @@ async function installService(p, io, o) {
|
|
|
1783
1789
|
if (platform === 'darwin')
|
|
1784
1790
|
svcEnv.ACH_AUTH_EXIT_CODE = '0';
|
|
1785
1791
|
mkdirSync(join(o.home, 'logs'), { recursive: true, mode: 0o700 });
|
|
1792
|
+
const self = fileURLToPath(import.meta.url);
|
|
1793
|
+
const npx = /[\\/]_npx[\\/]/.test(self) ? (whichAll('npx', env).find((x) => !isBundledBin(x)) ?? join(dirname(process.execPath), 'npx')) : undefined;
|
|
1786
1794
|
const f = serviceFile({
|
|
1787
1795
|
platform,
|
|
1788
1796
|
agentName: o.agentName,
|
|
1789
|
-
|
|
1790
|
-
cli: fileURLToPath(import.meta.url),
|
|
1797
|
+
command: npx ? [npx, '-y', npmSpec()] : [process.execPath, self],
|
|
1791
1798
|
args,
|
|
1792
1799
|
env: svcEnv,
|
|
1793
1800
|
logFile: join(o.home, 'logs', 'runner.log'),
|
|
1794
1801
|
}, env.ACH_SERVICE_DIR || undefined);
|
|
1795
1802
|
writeServiceFile(f);
|
|
1803
|
+
const load = !env.ACH_SERVICE_DIR;
|
|
1804
|
+
let failed = '';
|
|
1805
|
+
if (load) {
|
|
1806
|
+
for (const c of f.start) {
|
|
1807
|
+
try {
|
|
1808
|
+
execFileSync('/bin/sh', ['-c', c], { env, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
1809
|
+
}
|
|
1810
|
+
catch (e) {
|
|
1811
|
+
failed = `${c}: ${String(e?.stderr ?? e?.message ?? e).trim().split('\n')[0]}`;
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
// Linux: a user unit stops at logout (an SSH session ending) unless the user lingers
|
|
1817
|
+
const linger = platform === 'linux' && load && !failed ? lingers(env) : true;
|
|
1818
|
+
const logs = platform === 'linux' ? `journalctl --user -u ${basename(f.path)} -f` : join(o.home, 'logs', 'runner.log');
|
|
1796
1819
|
if (v.json) {
|
|
1797
|
-
io.out(JSON.stringify({ path: f.path, start: f.start, stop: f.stop }, null, 2));
|
|
1798
|
-
return 0;
|
|
1820
|
+
io.out(JSON.stringify({ path: f.path, started: load && !failed, ...(failed ? { error: failed } : {}), start: f.start, stop: f.stop }, null, 2));
|
|
1821
|
+
return failed ? 1 : 0;
|
|
1799
1822
|
}
|
|
1800
1823
|
io.out([
|
|
1801
|
-
`wrote ${f.path} (not loaded)`,
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
...
|
|
1806
|
-
`Runner output: ${join(o.home, 'logs', 'runner.log')}`,
|
|
1824
|
+
!load ? `wrote ${f.path} (not loaded: ACH_SERVICE_DIR is set). Start it with:` : failed ? `wrote ${f.path}, but starting it failed: ${failed}\nStart it with:` : `installed and started ${f.path} (also starts at login)`,
|
|
1825
|
+
...(!load || failed ? f.start.map((x) => ` ${x}`) : []),
|
|
1826
|
+
`Stop it with: ${f.stop.join(' && ')}`,
|
|
1827
|
+
`Runner output: ${logs}`,
|
|
1828
|
+
...(linger ? [] : ['Note: it stops when you log out of this machine. To keep it running: sudo loginctl enable-linger "$USER"']),
|
|
1807
1829
|
...(passEnv.length ? [`Note: the unit passes --pass-env ${passEnv.join(',')} but not their values; add them to the unit's environment yourself.`] : []),
|
|
1808
1830
|
].join('\n'));
|
|
1809
|
-
return 0;
|
|
1831
|
+
return failed ? 1 : 0;
|
|
1832
|
+
}
|
|
1833
|
+
/** Linux: does this user's systemd outlive their logins (loginctl linger)? Unknown counts as yes (no false alarm). */
|
|
1834
|
+
function lingers(env) {
|
|
1835
|
+
try {
|
|
1836
|
+
return execFileSync('loginctl', ['show-user', env.USER || String(process.getuid?.() ?? ''), '-p', 'Linger', '--value'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() !== 'no';
|
|
1837
|
+
}
|
|
1838
|
+
catch {
|
|
1839
|
+
return true;
|
|
1840
|
+
}
|
|
1810
1841
|
}
|
|
1811
1842
|
function isEntry() {
|
|
1812
1843
|
try {
|
package/dist/src/desknotify.d.ts
CHANGED
|
@@ -123,6 +123,8 @@ export declare class DeskNotifier {
|
|
|
123
123
|
private baseline;
|
|
124
124
|
private timer;
|
|
125
125
|
private busy;
|
|
126
|
+
/** The hub has no notification list (404): stop asking, say so once. */
|
|
127
|
+
private off;
|
|
126
128
|
private feed;
|
|
127
129
|
private linuxActions?;
|
|
128
130
|
/** Click handlers still waiting (tests await them). */
|
package/dist/src/desknotify.js
CHANGED
|
@@ -242,6 +242,8 @@ export class DeskNotifier {
|
|
|
242
242
|
baseline;
|
|
243
243
|
timer = null;
|
|
244
244
|
busy = false;
|
|
245
|
+
/** The hub has no notification list (404): stop asking, say so once. */
|
|
246
|
+
off = false;
|
|
245
247
|
feed = null;
|
|
246
248
|
linuxActions;
|
|
247
249
|
/** Click handlers still waiting (tests await them). */
|
|
@@ -286,7 +288,7 @@ export class DeskNotifier {
|
|
|
286
288
|
}
|
|
287
289
|
/** One poll: show new alert items (oldest first). Returns how many were shown. */
|
|
288
290
|
async tick() {
|
|
289
|
-
if (this.busy)
|
|
291
|
+
if (this.busy || this.off)
|
|
290
292
|
return 0;
|
|
291
293
|
this.busy = true;
|
|
292
294
|
try {
|
|
@@ -325,6 +327,11 @@ export class DeskNotifier {
|
|
|
325
327
|
return shown;
|
|
326
328
|
}
|
|
327
329
|
catch (e) {
|
|
330
|
+
if (e?.status === 404) {
|
|
331
|
+
this.off = true;
|
|
332
|
+
this.o.log?.('ach run --notify: this hub has no notification list; desktop notifications are off');
|
|
333
|
+
return 0;
|
|
334
|
+
}
|
|
328
335
|
this.o.log?.(`ach run --notify: ${oneLine(e?.message ?? e, 200)}`);
|
|
329
336
|
return 0;
|
|
330
337
|
}
|
package/dist/src/doctor.js
CHANGED
|
@@ -206,7 +206,7 @@ export async function runDoctor(o) {
|
|
|
206
206
|
const self = o.self ?? thisCliPath();
|
|
207
207
|
const achs = whichAll('ach', env).map((p) => identifyAch(p, self));
|
|
208
208
|
if (!achs.length) {
|
|
209
|
-
add('ach on PATH', 'warn', 'no `ach` command on PATH', 'machine', `npm install -g ${npmSpec()} (needed only to type \`ach\` yourself
|
|
209
|
+
add('ach on PATH', 'warn', 'no `ach` command on PATH', 'machine', `npm install -g ${npmSpec()} (needed only to type \`ach\` yourself)`);
|
|
210
210
|
}
|
|
211
211
|
else {
|
|
212
212
|
const desc = (a) => `${a.path}${a.real && a.real !== a.path ? ` -> ${a.real}` : ''} (${a.ours ? 'this CLI' : `${a.name ?? 'unknown'} ${a.version ?? ''}`.trim()})`;
|
|
@@ -220,17 +220,26 @@ export async function runDoctor(o) {
|
|
|
220
220
|
for (const a of achs.slice(1))
|
|
221
221
|
add('ach (shadowed)', isOurs(a) ? 'info' : 'warn', desc(a));
|
|
222
222
|
}
|
|
223
|
-
|
|
224
|
-
add('install', 'warn', 'this `ach` runs from the npx cache, which npm may clear', 'machine', `npm install -g ${npmSpec()}`);
|
|
225
|
-
}
|
|
226
|
-
// Background runner (ach run --install-service): is a unit file there?
|
|
223
|
+
// Background runner (ach run --install-service): installed, and running?
|
|
227
224
|
if (agentName) {
|
|
228
225
|
try {
|
|
229
|
-
const { serviceFile } = await import('./service.js');
|
|
226
|
+
const { serviceFile, serviceLabel } = await import('./service.js');
|
|
230
227
|
const platform = (env.ACH_SERVICE_PLATFORM || process.platform);
|
|
231
228
|
if (platform === 'darwin' || platform === 'linux') {
|
|
232
|
-
const f = serviceFile({ platform, agentName,
|
|
233
|
-
|
|
229
|
+
const f = serviceFile({ platform, agentName, command: [], args: [], env: {}, logFile: '' }, env.ACH_SERVICE_DIR || undefined);
|
|
230
|
+
if (!existsSync(f.path))
|
|
231
|
+
add('background', 'info', 'no background runner on this machine (optional: ach run --dir <repo> --install-service --interactive-first)');
|
|
232
|
+
else {
|
|
233
|
+
const unit = f.path.split(/[\\/]/).pop();
|
|
234
|
+
const r = platform === 'linux'
|
|
235
|
+
? await exec('systemctl', ['--user', 'is-active', unit], env)
|
|
236
|
+
: await exec('launchctl', ['print', `gui/${process.getuid?.() ?? ''}/${serviceLabel(agentName)}`], env);
|
|
237
|
+
const running = platform === 'linux' ? r.stdout.trim() === 'active' : r.code === 0 && /state = running/.test(r.stdout);
|
|
238
|
+
if (running)
|
|
239
|
+
add('background', 'ok', `tasks are picked up in the background (${f.path})`);
|
|
240
|
+
else
|
|
241
|
+
add('background', 'warn', `the background runner is installed but not running (${f.path})`, 'machine', f.start.join(' && '));
|
|
242
|
+
}
|
|
234
243
|
}
|
|
235
244
|
}
|
|
236
245
|
catch {
|
package/dist/src/onboard.d.ts
CHANGED
|
@@ -5,8 +5,9 @@ import type { ExecLike } from './doctor.js';
|
|
|
5
5
|
* 1. connect: add the `ach` MCP server to the agent's own tool (Claude Code
|
|
6
6
|
* for a claude agent, Codex for a codex one; both found ones for "other")
|
|
7
7
|
* 2. background helper, only with --service: `ach run --install-service`
|
|
8
|
-
* (+ --notify on a machine with a desktop)
|
|
9
|
-
*
|
|
8
|
+
* (+ --notify on a machine with a desktop), which installs and starts it.
|
|
9
|
+
* Run through npx, the helper runs the same pinned npx spec as the MCP
|
|
10
|
+
* entry, never a path in the npx cache.
|
|
10
11
|
*
|
|
11
12
|
* Pasting the line is the consent: with no terminal (an agent ran it) the steps
|
|
12
13
|
* run and say what they ran. In a terminal each step lists its commands and
|
|
@@ -67,7 +68,6 @@ export interface Detected {
|
|
|
67
68
|
/** User-installed claude / codex (never the bins npx puts on PATH). */
|
|
68
69
|
claude?: string;
|
|
69
70
|
codex?: string;
|
|
70
|
-
npm?: string;
|
|
71
71
|
npx: boolean;
|
|
72
72
|
/** An `ach` entry already registered (it is replaced). */
|
|
73
73
|
claudeHasAch: boolean;
|
|
@@ -86,7 +86,6 @@ export declare function detect(env: NodeJS.ProcessEnv, exec: ExecLike, self: str
|
|
|
86
86
|
export declare function planSteps(d: Detected, o: {
|
|
87
87
|
home?: string;
|
|
88
88
|
achCmd: Argv;
|
|
89
|
-
installSpec: string;
|
|
90
89
|
cwd: string;
|
|
91
90
|
zh?: boolean;
|
|
92
91
|
env?: NodeJS.ProcessEnv;
|
package/dist/src/onboard.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
1
|
import { homedir } from 'node:os';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
|
-
import { identifyAch, isOurs, MCP_SERVER_NAME,
|
|
5
|
-
import { VERSION } from './version.js';
|
|
3
|
+
import { identifyAch, isOurs, MCP_SERVER_NAME, npxAch, thisCliPath, userBin, whichAll } from './invite.js';
|
|
6
4
|
/** The closing line: which tool to open and what to say to it. */
|
|
7
5
|
export function doneLine(tools, zh) {
|
|
8
6
|
const names = [...new Set(tools)].map((k) => (k === 'claude' ? 'Claude Code' : 'Codex'));
|
|
@@ -65,7 +63,7 @@ export async function detect(env, exec, self, probe) {
|
|
|
65
63
|
const codex = userBin('codex', env);
|
|
66
64
|
const npx = runFromNpx(self);
|
|
67
65
|
const d = {
|
|
68
|
-
claude, codex,
|
|
66
|
+
claude, codex, npx,
|
|
69
67
|
claudeHasAch: false, codexHasAch: false,
|
|
70
68
|
// run through npx: the MCP entry uses the same pinned spec as the join line
|
|
71
69
|
achCmd: npx ? npxAch() : absoluteAch(env, self),
|
|
@@ -111,17 +109,15 @@ export function planSteps(d, o) {
|
|
|
111
109
|
if (o.service) {
|
|
112
110
|
const notify = o.desktop !== false;
|
|
113
111
|
const helper = ['run', '--install-service', ...(notify ? ['--notify'] : []), '--interactive-first', '--dir', o.cwd];
|
|
114
|
-
const install = d.npx && d.npm ? [[d.npm, 'install', '-g', o.installSpec]] : [];
|
|
115
112
|
steps.push({
|
|
116
113
|
id: 'service',
|
|
117
114
|
question: [
|
|
118
115
|
zh
|
|
119
116
|
? `安装后台小助手(在 ${o.cwd} 里自动领取任务${notify ? ',并弹出系统通知' : ''})?会运行:`
|
|
120
117
|
: `Install the background helper (picks up tasks in ${o.cwd}${notify ? ' and shows desktop notifications' : ''})? This runs:`,
|
|
121
|
-
|
|
122
|
-
` ${shellLine(['ach', ...helper])}`,
|
|
118
|
+
` ${shellLine([...o.achCmd, ...helper])}`,
|
|
123
119
|
].join('\n') + '\n',
|
|
124
|
-
commands: [
|
|
120
|
+
commands: [[...o.achCmd, ...helper]],
|
|
125
121
|
});
|
|
126
122
|
}
|
|
127
123
|
return steps;
|
|
@@ -130,26 +126,16 @@ function yesNo(answer) {
|
|
|
130
126
|
const a = answer.trim().toLowerCase();
|
|
131
127
|
return !a || /^(y|yes|是|好|ok)$/.test(a);
|
|
132
128
|
}
|
|
133
|
-
/** The global `ach` after `npm install -g` (npm prefix -g)/bin/ach. */
|
|
134
|
-
async function globalAch(npm, exec, env) {
|
|
135
|
-
const r = await exec(npm, ['prefix', '-g'], env);
|
|
136
|
-
const prefix = r.stdout.trim().split('\n').pop()?.trim();
|
|
137
|
-
if (r.code !== 0 || !prefix)
|
|
138
|
-
return undefined;
|
|
139
|
-
const f = process.platform === 'win32' ? join(prefix, 'ach.cmd') : join(prefix, 'bin', 'ach');
|
|
140
|
-
return existsSync(f) ? f : undefined;
|
|
141
|
-
}
|
|
142
129
|
/** Run (or, with --dry-run, only print) the setup after `ach join`. */
|
|
143
130
|
export async function runSetup(io, o) {
|
|
144
131
|
const env = io.env;
|
|
145
132
|
const self = o.self ?? thisCliPath();
|
|
146
133
|
const d = await detect(env, io.exec, self, !o.dryRun);
|
|
147
|
-
const installSpec = npmSpec(VERSION);
|
|
148
134
|
const home = env.AGENTCHANNEL_HOME || undefined;
|
|
149
135
|
const result = { ran: [], skipped: [], failed: [], tools: [] };
|
|
150
136
|
const zh = !!o.zh;
|
|
151
137
|
const desktop = hasDesktop(env);
|
|
152
|
-
const plan = planSteps(d, { home, achCmd: d.achCmd,
|
|
138
|
+
const plan = planSteps(d, { home, achCmd: d.achCmd, cwd: io.cwd, zh, env, runtime: o.runtime, service: o.service, desktop });
|
|
153
139
|
if (o.dryRun) {
|
|
154
140
|
printOnly(io, d, plan, { zh, join: o.join });
|
|
155
141
|
return result;
|
|
@@ -192,18 +178,7 @@ export async function runSetup(io, o) {
|
|
|
192
178
|
continue;
|
|
193
179
|
}
|
|
194
180
|
if (s.id === 'service') {
|
|
195
|
-
|
|
196
|
-
let helper = s.commands.at(-1);
|
|
197
|
-
if (s.commands.length > 1) {
|
|
198
|
-
if (!(await runCmds(s.commands.slice(0, -1)))) {
|
|
199
|
-
result.failed.push('service');
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
const g = await globalAch(d.npm, io.exec, env);
|
|
203
|
-
if (g)
|
|
204
|
-
helper = [g, ...helper.slice(d.achCmd.length)];
|
|
205
|
-
}
|
|
206
|
-
const ok = await installAndStartService(io, helper);
|
|
181
|
+
const ok = await installService(io, s.commands[0]);
|
|
207
182
|
(ok ? result.ran : result.failed).push('service');
|
|
208
183
|
continue;
|
|
209
184
|
}
|
|
@@ -223,36 +198,22 @@ export async function runSetup(io, o) {
|
|
|
223
198
|
}
|
|
224
199
|
return result;
|
|
225
200
|
}
|
|
226
|
-
/** `ach run --install-service --json
|
|
227
|
-
async function
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
if (r.code !== 0) {
|
|
231
|
-
io.err(` failed (exit ${r.code}): ${shellLine(argv)}${r.stderr.trim() ? `\n ${r.stderr.trim().split('\n')[0]}` : ''}`);
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
let f;
|
|
201
|
+
/** `ach run --install-service --json`: it writes the unit and starts it (not under ACH_SERVICE_DIR: tests). */
|
|
202
|
+
async function installService(io, argv) {
|
|
203
|
+
const r = await io.exec(argv[0], [...argv.slice(1), '--json'], io.env);
|
|
204
|
+
let f = {};
|
|
235
205
|
try {
|
|
236
206
|
f = JSON.parse(r.stdout);
|
|
237
207
|
}
|
|
238
208
|
catch {
|
|
239
|
-
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
io.out(` wrote ${f.path}`);
|
|
243
|
-
const starts = f.start.map((x) => x.replace(/\s+#.*$/, '')).filter((x) => !/loginctl/.test(x));
|
|
244
|
-
if (env.ACH_SERVICE_DIR) {
|
|
245
|
-
io.out(` start it with: ${starts.join(' && ')}`);
|
|
246
|
-
return true;
|
|
209
|
+
/* reported below */
|
|
247
210
|
}
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return false;
|
|
253
|
-
}
|
|
211
|
+
if (r.code !== 0 || !f.path) {
|
|
212
|
+
const why = f.error || r.stderr.trim().split('\n')[0];
|
|
213
|
+
io.err(` failed (exit ${r.code}): ${shellLine(argv)}${why ? `\n ${why}` : ''}`);
|
|
214
|
+
return false;
|
|
254
215
|
}
|
|
255
|
-
io.out(
|
|
216
|
+
io.out(f.started ? ` installed and started ${f.path}` : ` wrote ${f.path}; start it with: ${(f.start ?? []).join(' && ')}`);
|
|
256
217
|
return true;
|
|
257
218
|
}
|
|
258
219
|
/** --dry-run: print what would be run (nothing is run, not even read-only probes). */
|
package/dist/src/service.d.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* P4: `ach run --install-service` writes a macOS launchd agent or a Linux
|
|
3
|
-
* systemd user unit that starts this runner at login and restarts it
|
|
4
|
-
*
|
|
3
|
+
* systemd user unit that starts this runner at login and restarts it, then
|
|
4
|
+
* loads it (startService).
|
|
5
5
|
*/
|
|
6
6
|
export interface ServiceSpec {
|
|
7
7
|
platform: 'darwin' | 'linux';
|
|
8
8
|
agentName: string;
|
|
9
|
-
/** node
|
|
10
|
-
|
|
11
|
-
cli: string;
|
|
9
|
+
/** What starts the CLI: node + cli.js of an install, or `npx -y <spec>` when it runs from the npx cache. */
|
|
10
|
+
command: string[];
|
|
12
11
|
/** `run` arguments, without --install-service. */
|
|
13
12
|
args: string[];
|
|
14
13
|
env: Record<string, string>;
|
|
@@ -25,5 +24,5 @@ export declare function serviceLabel(agentName: string): string;
|
|
|
25
24
|
export declare function launchdPlist(s: ServiceSpec): string;
|
|
26
25
|
export declare function systemdUnit(s: ServiceSpec): string;
|
|
27
26
|
export declare function serviceFile(s: ServiceSpec, dir?: string): ServiceFile;
|
|
28
|
-
/** Write the unit file (mode 0644; it holds no secrets).
|
|
27
|
+
/** Write the unit file (mode 0644; it holds no secrets). */
|
|
29
28
|
export declare function writeServiceFile(f: ServiceFile): void;
|
package/dist/src/service.js
CHANGED
|
@@ -14,7 +14,7 @@ export function serviceLabel(agentName) {
|
|
|
14
14
|
return `io.agentchannel.runner.${agentName.replace(/[^A-Za-z0-9-]/g, '-')}`;
|
|
15
15
|
}
|
|
16
16
|
export function launchdPlist(s) {
|
|
17
|
-
const progArgs = [s.
|
|
17
|
+
const progArgs = [...s.command, 'run', ...s.args].map((a) => ` <string>${xml(a)}</string>`).join('\n');
|
|
18
18
|
const env = Object.entries(s.env)
|
|
19
19
|
.map(([k, v]) => ` <key>${xml(k)}</key>\n <string>${xml(v)}</string>`)
|
|
20
20
|
.join('\n');
|
|
@@ -50,7 +50,7 @@ ${env}
|
|
|
50
50
|
`;
|
|
51
51
|
}
|
|
52
52
|
export function systemdUnit(s) {
|
|
53
|
-
const exec = [s.
|
|
53
|
+
const exec = [...s.command, 'run', ...s.args].map(sd).join(' ');
|
|
54
54
|
const env = Object.entries(s.env)
|
|
55
55
|
.map(([k, v]) => `Environment=${sd(`${k}=${v}`)}`)
|
|
56
56
|
.join('\n');
|
|
@@ -79,7 +79,9 @@ export function serviceFile(s, dir) {
|
|
|
79
79
|
return {
|
|
80
80
|
path,
|
|
81
81
|
content: launchdPlist(s),
|
|
82
|
-
|
|
82
|
+
// bootout first: bootstrap refuses a label that is already loaded (a reinstall); bootout returns before the
|
|
83
|
+
// old job is gone, so a bootstrap right after it can fail once (error 5): try again after a moment
|
|
84
|
+
start: [`launchctl bootout gui/$(id -u)/${label} 2>/dev/null; launchctl bootstrap gui/$(id -u) ${path} 2>/dev/null || { sleep 2; launchctl bootstrap gui/$(id -u) ${path}; }`],
|
|
83
85
|
stop: [`launchctl bootout gui/$(id -u)/${label}`],
|
|
84
86
|
};
|
|
85
87
|
}
|
|
@@ -88,11 +90,12 @@ export function serviceFile(s, dir) {
|
|
|
88
90
|
return {
|
|
89
91
|
path,
|
|
90
92
|
content: systemdUnit(s),
|
|
91
|
-
|
|
93
|
+
// restart, not `enable --now`: a reinstall must pick up the new unit
|
|
94
|
+
start: ['systemctl --user daemon-reload', `systemctl --user enable ${unit}`, `systemctl --user restart ${unit}`],
|
|
92
95
|
stop: [`systemctl --user disable --now ${unit}`],
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
|
-
/** Write the unit file (mode 0644; it holds no secrets).
|
|
98
|
+
/** Write the unit file (mode 0644; it holds no secrets). */
|
|
96
99
|
export function writeServiceFile(f) {
|
|
97
100
|
const dir = join(f.path, '..');
|
|
98
101
|
if (!existsSync(dir))
|
package/dist/src/version.d.ts
CHANGED
package/dist/src/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koalaauto/agentchannel",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "Hand work between your team's agents (Claude Code, Codex) and follow it in one panel: the ach CLI, MCP server and runner for AgentChannel v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentchannel",
|