@ctliz/agent-intercom-pi 0.12.2 → 0.14.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
@@ -17,6 +17,76 @@
17
17
  | AGY | [`agent-intercom-agy`](https://github.com/ctliz/agent-intercom-agy) |
18
18
  | Fleet lifecycle | [`agent-intercom-orchestrator`](https://github.com/ctliz/agent-intercom-orchestrator) |
19
19
 
20
+ ## Pi 1.0 compatibility and codemode
21
+
22
+ Version 0.14.0 is tested with Pi 1.0.0. The preceding 0.13.0 release was tested with Pi 0.99.1. The development test baseline now uses Pi 1.0.0; host-provided Pi modules remain peer dependencies, not runtime dependencies. The existing protocol v4 broker, durable queues, acknowledgements, and cross-harness routing are unchanged.
23
+
24
+ Pi 1.0 defaults to fullscreen; set `tuiMode` to `"regular"` or launch with `--tui-mode regular` to retain terminal scrollback. Codemode's shorter declarations preserve Intercom's structured `{ ok, text, data }` results and team guidelines. To check tool existence in a script, use `"intercom_send" in tools`, not `typeof tools.intercom_send`, because unknown members now throw. Restart existing Pi processes to use a newly installed Pi version; `/reload` only reloads resources inside the running version.
25
+
26
+ All Intercom tools remain directly callable and are also available through Pi's built-in codemode when active. Scripts receive `{ ok, text, data }` rather than a display string. `data` contains the tool's structured details: delivery flags and message ID, session lists, team roster, pending asks, or connection status. Returned failures set `isError: true` and retain structured data; a deferred ask is successful with `data.pending === true`. Always check `ok` before continuing with dependent actions. Blocked calls, invalid arguments, and thrown exceptions can still reject, so use `try/catch` or `Promise.allSettled()` when appropriate.
27
+
28
+ ```javascript
29
+ const sessions = await tools.intercom_list({});
30
+ if (!sessions.ok) throw new Error(sessions.text);
31
+ const target = sessions.data.sessions.find(s => s.name === "worker");
32
+ if (!target) throw new Error("Worker is offline");
33
+ const result = await tools.intercom_send({ to: target.id, message: "Tests passed." });
34
+ return { ok: result.ok, accepted: result.data.accepted, delivered: result.data.delivered };
35
+ ```
36
+
37
+ Concurrent sends and independent asks are supported; do not create two unresolved asks to the same recipient. Team joins execute sequentially; named task-team updates are also locked across independently launched Pi processes. Named joins append membership without changing the caller's broker routing scope. Receiving a delivery acknowledgement means the message is durably queued, not that the recipient finished its task. Busy sessions wait until `ctx.isIdle()`; `agent_settled` updates final idle status after automatic retries and compaction.
38
+
39
+ ### Send from a shell or release script
40
+
41
+ The package includes an `intercom-send` executable. Install the alias globally for a shell command, or run it without relying on Pi's private installation path:
42
+
43
+ ```bash
44
+ npm exec --yes --package=@ctliz/pi-intercom@0.14.0 -- intercom-send worker 'Tests passed.'
45
+ ```
46
+
47
+ It prints one JSON result with `accepted`, `delivered`, `messageId`, and optional failure `code`/`reason`. Exit status is zero only for acknowledged delivery. It inherits the routing scope, but never inherits `PI_INTERCOM_SESSION_ID` or `AGENT_INTERCOM_SESSION_ID`: every invocation registers an independent sender, leaves running Pi sessions intact, and disconnects after sending. It is send-only; use the session tools for reply-tracked asks.
48
+
49
+ When a second runtime claims the same stable session ID, Intercom reports `SESSION_ID_IN_USE`, pauses automatic reconnect, and preserves the original owner. Switch to a different session, or release the duplicate owner and `/reload`. `intercom_status` exposes the conflict as structured data rather than silently treating it as a temporary outage. Updating this adapter does not require restarting a compatible v4 broker.
50
+
51
+ ## Pi-local task teams
52
+
53
+ Open Pi normally in separate terminals; no startup team, session name for the
54
+ coordinator, or terminal multiplexer is required. When you ask an agent to work
55
+ with `front` and `writer`, its tool guidelines tell it to ask once whether to form
56
+ a team. After approval (or an explicit request to form a team), it discovers the
57
+ sessions and creates the task group itself:
58
+
59
+ ```typescript
60
+ intercom_join({ name: "launch", create: true, members: ["front", "writer"], work: "Build the product page" })
61
+ intercom_send({ team: "launch", to: "front", message: "Implement the UI." })
62
+ intercom_send({ team: "launch", to: "writer", message: "Write the copy." })
63
+ intercom_team({}) // all your task teams
64
+ intercom_team({ team: "launch" }) // one team's manager and members
65
+ ```
66
+
67
+ Joining another named team appends membership, never replaces it. The manager can
68
+ append approved peers with `members`; any session can join itself. Missing or
69
+ ambiguous targets fail before membership is written. Team updates are persistent
70
+ and serialized across Pi processes. A different task can use a different team
71
+ with the same participants.
72
+
73
+ Every task message carries a public team name, displayed as `[Team: launch]`.
74
+ When two sessions share multiple teams, sends/asks require `team`. Replies inherit
75
+ the exact original message's team: use its receiver-local `contextId` reply hint,
76
+ or an `askId` from `intercom_pending`. Mixed-team batches retain every message's
77
+ label; ambiguous replies fail rather than guessing.
78
+
79
+ This feature changes only Pi's named teams and prompt guidelines. It does not
80
+ change the shared v4 broker or other adapters, does not add a security boundary,
81
+ and does not isolate model history per team. Without a shared team, omitting
82
+ `team` sends an ungrouped direct message even when either session belongs to
83
+ unrelated teams, allowing initial contact before forming a team. Explicit team
84
+ messages still require both sessions to be members; multiple shared teams still
85
+ require selecting `team`. Consent is an agent instruction, not a broker-enforced approval record.
86
+ Managed-team and workspace integration paths are unchanged. All participating Pi
87
+ sessions need the 0.14.0 Pi adapter and `/reload`. This is not a cross-adapter
88
+ protocol rollout.
89
+
20
90
  ## Grok Build and AGY support
21
91
 
22
92
  Grok Build and AGY are supported as first-class protocol peers through two dedicated npm packages:
@@ -93,7 +163,7 @@ Each pi session that has `pi-intercom` loaded and enabled connects to a tiny loc
93
163
  ## Install
94
164
 
95
165
  ```bash
96
- pi install git:github.com/ctliz/agent-intercom-pi@v0.12.2
166
+ pi install npm:@ctliz/pi-intercom@0.14.0
97
167
  ```
98
168
 
99
169
  If you are coming from `connect.1`, read [Upgrading from `connect.1`](#upgrading-from-connect1-to-connect2) first — the package namespace changed and the two versions must not be installed side by side.
@@ -292,7 +362,7 @@ If you never set `/name`, Intercom still exposes a runtime-only fallback alias s
292
362
 
293
363
  ### How `intercom_team` chooses a team
294
364
 
295
- `intercom_team({})` has no arguments. It resolves the current group in this order and stops at the first match:
365
+ With Pi-local task teams, `intercom_team({})` first shows all named teams containing this session; `intercom_team({ team: "billing" })` selects one. If there are no named task teams, the existing managed-team resolution below applies. Boss mode keeps its existing resolution and does not accept named task teams.
296
366
 
297
367
  1. **Orchestrator** — `~/.pi/agent/intercom/orchestrator/workers.json` has an owned record for this session (`AGENT_INTERCOM_WORKER_ID`) or this session is the current manager of live owned coworkers.
298
368
  2. **TmuxDeck manifest** — `AGENT_INTERCOM_TEAM_MANIFEST` points at a valid team file. The Lead is `leadId`; workers are the other members. An invalid or empty manifest fails closed and does not fall through to the live roster.
@@ -321,7 +391,7 @@ Manifest, live-roster, and standalone teams cannot inspect another session's inb
321
391
 
322
392
  ### Create or join a team without tmux
323
393
 
324
- Named teams live in the local Intercom directory. Creating one generates a private scope, makes this session the manager, and is enough for `intercom_team` to return a live roster. Tmux and TmuxDeck are not required.
394
+ Named teams live in the local Intercom directory. Creating one makes this session the task-team manager and stores explicit member session IDs. Joining appends membership without switching broker scope or replacing previous memberships. Legacy records retain their manager; peers must join again to record their membership. Tmux and TmuxDeck are not required.
325
395
 
326
396
  ```text
327
397
  /intercom-create billing # create a named team and join as manager
@@ -601,11 +671,11 @@ The supervisor can reply with plain JSON or a fenced `json` block. If the reply
601
671
 
602
672
  | Tool | Parameters | Description |
603
673
  |------|------------|-------------|
604
- | `intercom_send` | required `to`, required `message`, optional `attachments` | Fire-and-forget delivery |
605
- | `intercom_ask` | required `to`, required `message`, optional `attachments` | Ask and wait briefly for a reply |
606
- | `intercom_reply` | required `message`, optional `askId`, `to`, `which` | Reply to the active or pending inbound message; `askId` selects an exact unresolved ask, while `to`/`which` remain compatible selectors |
607
- | `intercom_team` | none | Show the current manager and live coworkers owned by that manager |
608
- | `intercom_join` | optional `name`, optional `create` | List, join, or create a named team without tmux |
674
+ | `intercom_send` | required `to`, `message`; optional `team`, `attachments` | Fire-and-forget delivery in a task team |
675
+ | `intercom_ask` | required `to`, `message`; optional `team`, `attachments` | Ask and wait briefly for a reply |
676
+ | `intercom_reply` | required `message`; optional `contextId`, `askId`, `team`, `to`, `which` | Select an exact inbound context and inherit its team |
677
+ | `intercom_team` | optional `team` | Show your named task teams, or fall back to managed-team discovery |
678
+ | `intercom_join` | optional `name`, `create`, `members`, `work` | Append membership; a manager can add connected peers, and `work` describes a newly created task |
609
679
  | `intercom_list` | none | List connected sessions in your scope |
610
680
  | `intercom_pending` | optional `askId`, `session` | List unresolved inbound asks with stable IDs; `askId` retrieves the full untruncated body, and managers may use `session` for an owned coworker |
611
681
  | `intercom_status` | none | Show connection and queue status |
@@ -630,7 +700,7 @@ Only registered in sessions where `pi-subagents` supplied the required child bri
630
700
 
631
701
  ### Tool behavior
632
702
 
633
- **`intercom_team`** reads orchestrator ownership dynamically and returns the current manager plus live same-manager coworkers. After adoption it follows the new manager without restarting the worker; `AGENT_INTERCOM_MANAGER_TARGET` is only a startup fallback.
703
+ **`intercom_team`** first shows your named Pi task teams. Without named memberships it reads orchestrator ownership dynamically and returns the current manager plus live same-manager coworkers. After adoption it follows the new manager without restarting the worker; `AGENT_INTERCOM_MANAGER_TARGET` is only a startup fallback.
634
704
 
635
705
  **`intercom_join`** lists named teams and TmuxDeck workspaces, joins one by name, or creates a named team with `create: true`. Creating a team does not require tmux. Listing never prints the raw scope.
636
706
 
@@ -640,7 +710,7 @@ Only registered in sessions where `pi-subagents` supplied the required child bri
640
710
 
641
711
  **`intercom_ask`** waits up to 30 seconds for a prompt reply, then returns a successful pending result while keeping the request open for a late reply. Different recipients may wait concurrently; the same recipient may have only one unresolved ask. `PI_INTERCOM_ASK_WAIT_MS` changes the blocking window.
642
712
 
643
- **`intercom_reply`** resolves the active or pending inbound context internally. Pass the stable `askId` returned by `intercom_pending` to select one exact ask. Optional `to` and `which: "oldest" | "latest"` remain available for compatibility. None of these values exposes the protocol thread ID.
713
+ **`intercom_reply`** resolves the active or pending inbound context internally and inherits its original team. Use the `contextId` in a team message's reply hint for an exact ordinary message, or pass the stable `askId` returned by `intercom_pending` to select one exact ask. A `team` selector must match the original message, never override it. Optional `to` and `which: "oldest" | "latest"` remain available for compatibility. None of these values exposes the protocol thread ID.
644
714
 
645
715
  The broker refuses a second unresolved `intercom_ask` from one session to the same recipient. Wait for the first answer or use `intercom_send` for a non-blocking follow-up.
646
716
 
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { register } from "tsx/esm/api";
3
+ register();
4
+ await import("../cli-send.ts");
package/broker/client.ts CHANGED
@@ -38,6 +38,7 @@ import type {
38
38
 
39
39
  export interface SendOptions {
40
40
  text: string;
41
+ team?: string;
41
42
  attachments?: Attachment[];
42
43
  control?: IntercomCommonControlEnvelope;
43
44
  replyTo?: string;
@@ -125,6 +126,8 @@ function isMessage(value: unknown): value is Message {
125
126
  return false;
126
127
  }
127
128
 
129
+ if (content.team !== undefined && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
130
+
128
131
  const attachmentsValid = content.attachments === undefined
129
132
  || (Array.isArray(content.attachments) && content.attachments.every(isAttachment));
130
133
  const controlValid = content.control === undefined || isIntercomCommonControlEnvelope(content.control);
@@ -376,7 +379,9 @@ export class IntercomClient extends EventEmitter {
376
379
  };
377
380
 
378
381
  const onReaderError = (error: Error) => {
379
- const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error });
382
+ const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error }) as Error & { code?: string };
383
+ const code = (error as Error & { code?: string }).code;
384
+ if (code) protocolError.code = code;
380
385
  if (!connectionEstablished) {
381
386
  onError(protocolError);
382
387
  return;
@@ -809,6 +814,7 @@ export class IntercomClient extends EventEmitter {
809
814
  expectsReply: options.expectsReply,
810
815
  content: {
811
816
  text: options.text,
817
+ ...(options.team === undefined ? {} : { team: options.team }),
812
818
  attachments: options.attachments,
813
819
  control: options.control,
814
820
  },
package/broker/framing.ts CHANGED
@@ -41,7 +41,10 @@ export function createMessageReader(
41
41
  return true;
42
42
  } catch (error) {
43
43
  const message = error instanceof Error ? error.message : String(error);
44
- onError(new Error(`Failed to handle intercom message: ${message}`, { cause: error }));
44
+ const handlerError = new Error(`Failed to handle intercom message: ${message}`, { cause: error }) as Error & { code?: string };
45
+ const code = (error as { code?: unknown } | null)?.code;
46
+ if (typeof code === "string") handlerError.code = code;
47
+ onError(handlerError);
45
48
  return false;
46
49
  }
47
50
  }
package/cli-send.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { IntercomClient } from "./broker/client.ts";
3
+ import { spawnBrokerIfNeeded } from "./broker/spawn.ts";
4
+ import { loadConfig } from "./config.ts";
5
+
6
+ async function main(): Promise<void> {
7
+ const [to, ...parts] = process.argv.slice(2);
8
+ const message = parts.join(" ");
9
+ if (!to?.trim() || !message.trim()) {
10
+ throw new Error("Usage: intercom-send <session-name-or-id> <message>");
11
+ }
12
+ const config = loadConfig();
13
+ if (!config.enabled) throw new Error("Intercom disabled");
14
+ await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
15
+ const client = new IntercomClient();
16
+ const now = Date.now();
17
+ try {
18
+ // Never inherit the calling Pi's session ID or take over its mailbox.
19
+ await client.connect({
20
+ name: `intercom-cli-${randomUUID()}`,
21
+ cwd: process.cwd(),
22
+ model: "cli-sender",
23
+ pid: process.pid,
24
+ startedAt: now,
25
+ lastActivity: now,
26
+ runtimeInstanceId: randomUUID(),
27
+ });
28
+ const result = await client.send(to, { text: message });
29
+ console.log(JSON.stringify({ messageId: result.id, ...result }));
30
+ if (!result.delivered) process.exitCode = 1;
31
+ } finally {
32
+ await client.disconnect();
33
+ }
34
+ }
35
+
36
+ main().catch((error: Error & { code?: string }) => {
37
+ console.log(JSON.stringify({ accepted: false, delivered: false, reason: error.message, ...(error.code ? { code: error.code } : {}) }));
38
+ process.exitCode = 1;
39
+ });