@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 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 it first installs this version globally, since a
44
- service cannot run from the npx cache.
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
@@ -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] (writes a launchd/systemd user unit; does not load it)
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. Writes the file and prints how to start it; it
1752
- * never loads the unit. ACH_SERVICE_DIR / ACH_SERVICE_PLATFORM override the
1753
- * location and platform (tests).
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 are on unless --no-notify
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
- const svcEnv = { PATH: env.PATH ?? '/usr/bin:/bin' };
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
- node: process.execPath,
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
- 'Start it with:',
1803
- ...f.start.map((x) => ` ${x}`),
1804
- 'Stop it with:',
1805
- ...f.stop.map((x) => ` ${x}`),
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 {
@@ -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). */
@@ -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
  }
@@ -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 or for the background helper: the join line with --service does it)`);
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
- if (/[\\/]_npx[\\/]/.test(self)) {
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, node: '', cli: '', args: [], env: {}, logFile: '' }, env.ACH_SERVICE_DIR || undefined);
233
- add('background', 'info', existsSync(f.path) ? `tasks are picked up in the background (${f.path})` : 'no background runner on this machine (optional: ach run --dir <repo> --install-service --interactive-first)');
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 {
@@ -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). Run through npx, `ach` is
9
- * installed first, so the helper never runs from the npx cache.
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;
@@ -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, npmSpec, npxAch, thisCliPath, userBin, whichAll } from './invite.js';
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, npm: whichAll('npm', env)[0], npx,
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
- ...install.map(() => ` npm install -g ${o.installSpec} # ${zh ? '`ach` 命令(后台小助手不能从 npx 缓存运行)' : 'the `ach` command (the helper cannot run from the npx cache)'}`),
122
- ` ${shellLine(['ach', ...helper])}`,
118
+ ` ${shellLine([...o.achCmd, ...helper])}`,
123
119
  ].join('\n') + '\n',
124
- commands: [...install, [...o.achCmd, ...helper]],
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, installSpec, cwd: io.cwd, zh, env, runtime: o.runtime, service: o.service, desktop });
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
- // run through npx: install `ach` first; the helper then runs the installed one, never the npx cache
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`, then start the unit (unless ACH_SERVICE_DIR points elsewhere: tests). */
227
- async function installAndStartService(io, argv) {
228
- const env = io.env;
229
- const r = await io.exec(argv[0], [...argv.slice(1), '--json'], env);
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
- io.err(' could not read the service file location');
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
- for (const c of starts) {
249
- const s = await io.exec('/bin/sh', ['-c', c], env);
250
- if (s.code !== 0) {
251
- io.err(` failed (exit ${s.code}): ${c}`);
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(' started');
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). */
@@ -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. It only
4
- * writes the file and prints how to start it; it never loads the unit.
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 binary and cli.js of this install. */
10
- node: string;
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). Never loads it. */
27
+ /** Write the unit file (mode 0644; it holds no secrets). */
29
28
  export declare function writeServiceFile(f: ServiceFile): void;
@@ -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.node, s.cli, 'run', ...s.args].map((a) => ` <string>${xml(a)}</string>`).join('\n');
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.node, s.cli, 'run', ...s.args].map(sd).join(' ');
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
- start: [`launchctl bootstrap gui/$(id -u) ${path}`],
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
- start: ['systemctl --user daemon-reload', `systemctl --user enable --now ${unit}`, `loginctl enable-linger "$USER" # optional: keep it running while logged out`],
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). Never loads it. */
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))
@@ -1,3 +1,3 @@
1
- export declare const VERSION = "3.2.0";
1
+ export declare const VERSION = "3.3.0";
2
2
  /** The npm package (scripts/release.mjs keeps it and VERSION in step with package.json). */
3
3
  export declare const PKG_NAME = "@koalaauto/agentchannel";
@@ -1,3 +1,3 @@
1
- export const VERSION = '3.2.0';
1
+ export const VERSION = '3.3.0';
2
2
  /** The npm package (scripts/release.mjs keeps it and VERSION in step with package.json). */
3
3
  export const PKG_NAME = '@koalaauto/agentchannel';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@koalaauto/agentchannel",
3
- "version": "3.2.0",
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",