@ctliz/agent-intercom-pi 0.12.0-connect.8 → 0.12.0-connect.9
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 +42 -12
- package/index.ts +147 -68
- package/named-teams.ts +250 -0
- package/package.json +1 -1
- package/skills/pi-intercom/SKILL.md +16 -4
- package/workspace-join.ts +8 -4
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Grok Build and AGY are supported as first-class protocol peers through two dedic
|
|
|
26
26
|
| Grok Build | [`@ctliz/agent-intercom-grok`](https://www.npmjs.com/package/@ctliz/agent-intercom-grok) | `agent-intercom-grok-mcp` |
|
|
27
27
|
| AGY | [`@ctliz/agent-intercom-agy`](https://www.npmjs.com/package/@ctliz/agent-intercom-agy) | `agent-intercom-agy-mcp` |
|
|
28
28
|
|
|
29
|
-
The host packages depend on `@ctliz/agent-intercom-claude` and load its MCP runtime internally. Users do **not** need to install or place `claude-intercom-mcp` on `PATH` separately. Once connected, Grok and AGY sessions share the same local broker and protocol as Pi, Codex, Claude Code, and OpenCode, and expose
|
|
29
|
+
The host packages depend on `@ctliz/agent-intercom-claude` and load its MCP runtime internally. Users do **not** need to install or place `claude-intercom-mcp` on `PATH` separately. Once connected, Grok and AGY sessions share the same local broker and protocol as Pi, Codex, Claude Code, and OpenCode, and expose the MCP operations: `intercom_whoami`, `intercom_list`, `intercom_send`, `intercom_ask`, `intercom_reply`, `intercom_pending`, `intercom_status`, `intercom_team`, `intercom_join`, and `intercom_set_summary`.
|
|
30
30
|
|
|
31
31
|
Install the host adapter before installing its plugin:
|
|
32
32
|
|
|
@@ -93,7 +93,7 @@ Each pi session that has `pi-intercom` loaded and enabled connects to a tiny loc
|
|
|
93
93
|
## Install
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.
|
|
96
|
+
pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.9
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
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.
|
|
@@ -141,7 +141,7 @@ A session becomes intercom-connected when all of these are true:
|
|
|
141
141
|
|
|
142
142
|
The session list only shows intercom-connected sessions, not every open Pi process on the machine.
|
|
143
143
|
|
|
144
|
-
If you upgrade pi-intercom or the orchestrator while sessions are already open, run `/reload` in each open Pi session (and restart any companion `coi`, `cci`, or OpenCode adapter). Update the packages by reinstalling the exact release tags with `pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.
|
|
144
|
+
If you upgrade pi-intercom or the orchestrator while sessions are already open, run `/reload` in each open Pi session (and restart any companion `coi`, `cci`, or OpenCode adapter). Update the packages by reinstalling the exact release tags with `pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.9` and, only where Orchestrator is actually installed, `pi install git:github.com/ctliz/agent-intercom-orchestrator@v0.12.0-connect.5`. Extensions are loaded into the running host process, so an existing session cannot adopt new broker/discovery code until it reloads. This is especially important when upgrading from a release that allowed multiple broker processes to form separate session-list "islands": the broker ownership fix prevents new splits, but it cannot move clients that are still running the old code. After every host has reloaded once, they converge on the same broker automatically.
|
|
145
145
|
|
|
146
146
|
If `/intercom` still reports no peers, first confirm the other Pi windows have pi-intercom loaded and have also been reloaded. Open Pi processes without the extension, disabled sessions, and sessions using a different `PI_CODING_AGENT_DIR` intentionally do not appear in the same list.
|
|
147
147
|
|
|
@@ -175,7 +175,7 @@ Press **Alt+I** or run `/intercom-id` to copy a short handoff snippet for the cu
|
|
|
175
175
|
|
|
176
176
|
### From the Agent
|
|
177
177
|
|
|
178
|
-
The agent uses
|
|
178
|
+
The agent uses eight focused tools: `intercom_send`, `intercom_ask`, `intercom_reply`, `intercom_team`, `intercom_join`, `intercom_list`, `intercom_pending`, and `intercom_status`. Tool calls and results render as compact transcript rows so coordination is easy to scan. For common patterns like planner-worker delegation, the bundled `pi-intercom` skill provides copy-paste ready examples:
|
|
179
179
|
|
|
180
180
|
```typescript
|
|
181
181
|
// Find the manager and managed coworkers without searching the global list
|
|
@@ -297,7 +297,7 @@ If you never set `/name`, Intercom still exposes a runtime-only fallback alias s
|
|
|
297
297
|
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
298
|
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.
|
|
299
299
|
3. **Same-scope live roster** — `AGENT_INTERCOM_SCOPE_ID` is set. Coworkers are the other live non-human sessions in that scope. `AGENT_INTERCOM_MANAGER_TARGET` / `AGENT_INTERCOM_MANAGER_SESSION_ID` name the Lead when present.
|
|
300
|
-
4. **Standalone** — no manager and no coworkers.
|
|
300
|
+
4. **Standalone** — no manager and no coworkers. Create a named team with `/intercom-create` or `intercom_join({ create: true })`, or use `/name` plus `intercom_list` / `intercom_send`.
|
|
301
301
|
|
|
302
302
|
Typical result:
|
|
303
303
|
|
|
@@ -319,16 +319,32 @@ intercom_pending({ session: "tmuxdeck-11111111-2222-4333-8444-555555555555" })
|
|
|
319
319
|
|
|
320
320
|
Manifest, live-roster, and standalone teams cannot inspect another session's inbox.
|
|
321
321
|
|
|
322
|
-
###
|
|
322
|
+
### Create or join a team without tmux
|
|
323
323
|
|
|
324
|
-
|
|
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.
|
|
325
325
|
|
|
326
326
|
```text
|
|
327
|
-
/intercom-
|
|
328
|
-
/intercom-join
|
|
327
|
+
/intercom-create billing # create a named team and join as manager
|
|
328
|
+
/intercom-join billing # join that named team from another session
|
|
329
|
+
/intercom-join # list named teams and TmuxDeck workspaces
|
|
329
330
|
/intercom-join 1 # join by the listed number
|
|
330
|
-
/intercom-join --scope <48hex> # join by the workspace scope
|
|
331
331
|
/intercom-status # confirm membership and visible peers
|
|
332
|
+
intercom_team({})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Agents can do the same without a slash command:
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
intercom_join({ name: "billing", create: true }) // create and join as manager
|
|
339
|
+
intercom_join({ name: "billing" }) // join an existing named team
|
|
340
|
+
intercom_join({}) // list joinable teams
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`/intercom-join` can still attach a standalone Pi session to an existing TmuxDeck workspace circle. That path is same-scope messaging only. It does **not** write a team manifest, does **not** enroll you as a Team Worker, and does **not** grant inbox inspection.
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
/intercom-join frontend # join by exact TmuxDeck workspace name
|
|
347
|
+
/intercom-join --scope <48hex> # join by the workspace scope
|
|
332
348
|
```
|
|
333
349
|
|
|
334
350
|
Managed Team Workers and Orchestrator-owned sessions cannot join another circle. Listing never prints the raw scope.
|
|
@@ -347,7 +363,18 @@ intercom_status({})
|
|
|
347
363
|
intercom_list({})
|
|
348
364
|
```
|
|
349
365
|
|
|
350
|
-
`intercom_list` should now show both `planner` and `worker`.
|
|
366
|
+
`intercom_list` should now show both `planner` and `worker`. They are still standalone until one session creates a named team:
|
|
367
|
+
|
|
368
|
+
```text
|
|
369
|
+
# Terminal 1
|
|
370
|
+
/intercom-create billing
|
|
371
|
+
|
|
372
|
+
# Terminal 2
|
|
373
|
+
/intercom-join billing
|
|
374
|
+
intercom_team({})
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
After that, `intercom_team` returns the pair as a live roster. If these terminals were launched inside the same TmuxDeck workspace, the live roster is already present without `/intercom-create`.
|
|
351
378
|
|
|
352
379
|
Planner assigns work:
|
|
353
380
|
|
|
@@ -374,7 +401,7 @@ intercom_send({
|
|
|
374
401
|
})
|
|
375
402
|
```
|
|
376
403
|
|
|
377
|
-
If a third Pi is already running as `reviewer
|
|
404
|
+
If a third Pi is already running as `reviewer`, join the named team from a new standalone window:
|
|
378
405
|
|
|
379
406
|
```text
|
|
380
407
|
/intercom-join billing
|
|
@@ -578,6 +605,7 @@ The supervisor can reply with plain JSON or a fenced `json` block. If the reply
|
|
|
578
605
|
| `intercom_ask` | required `to`, required `message`, optional `attachments` | Ask and wait briefly for a reply |
|
|
579
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 |
|
|
580
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 |
|
|
581
609
|
| `intercom_list` | none | List connected sessions in your scope |
|
|
582
610
|
| `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 |
|
|
583
611
|
| `intercom_status` | none | Show connection and queue status |
|
|
@@ -604,6 +632,8 @@ Only registered in sessions where `pi-subagents` supplied the required child bri
|
|
|
604
632
|
|
|
605
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.
|
|
606
634
|
|
|
635
|
+
**`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
|
+
|
|
607
637
|
**`intercom_list`** returns the current session plus other active intercom-connected sessions with name, short ID, working directory, model, and live status. Under protocol v4 it is same-scope: it returns only sessions sharing your scope (or only unscoped sessions when you are unscoped). Cross-scope contact is possible only by exact full session ID. Orchestrator-owned workers should still prefer `intercom_team` for their group.
|
|
608
638
|
|
|
609
639
|
**`intercom_send`** sends immediately and distinguishes broker `accepted` from receiver-acknowledged `delivered`. Set `confirmSend: true` for an interactive confirmation dialog.
|
package/index.ts
CHANGED
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
currentTmuxWorkspace,
|
|
25
25
|
formatJoinStatus,
|
|
26
26
|
formatJoinSuccess,
|
|
27
|
-
formatJoinableWorkspaceList,
|
|
28
27
|
isZhLocale,
|
|
29
28
|
listScopedWorkspaces,
|
|
30
29
|
parseJoinArgs,
|
|
@@ -32,6 +31,18 @@ import {
|
|
|
32
31
|
rejectManagedJoin,
|
|
33
32
|
workspaceNameForScope,
|
|
34
33
|
} from "./workspace-join.ts";
|
|
34
|
+
import {
|
|
35
|
+
buildJoinableCircles,
|
|
36
|
+
createNamedTeam,
|
|
37
|
+
findNamedTeamByScope,
|
|
38
|
+
formatCreateSuccess,
|
|
39
|
+
formatJoinableCircleList,
|
|
40
|
+
formatNamedJoinSuccess,
|
|
41
|
+
listNamedTeams,
|
|
42
|
+
parseCreateArgs,
|
|
43
|
+
parseTeamName,
|
|
44
|
+
resolveJoinCircle,
|
|
45
|
+
} from "./named-teams.ts";
|
|
35
46
|
import { authorizeBossSender, BossTeamScopeError, bossSelfSessionError, filterBossSessions, isBossControllerReadinessControl, readBossTeamScope, resolveBossLiveTarget } from "./boss-team-scope.ts";
|
|
36
47
|
import {
|
|
37
48
|
INTERCOM_CONTROL_DELIVERY_EVENT,
|
|
@@ -886,6 +897,76 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
886
897
|
}
|
|
887
898
|
await ensureConnected("tool");
|
|
888
899
|
}
|
|
900
|
+
function currentDisplayName(): string {
|
|
901
|
+
return currentSessionId
|
|
902
|
+
? buildPresenceIdentity(pi, currentSessionId).name
|
|
903
|
+
: (pi.getSessionName()?.trim() || "unnamed");
|
|
904
|
+
}
|
|
905
|
+
function rejectCircleChange(): string | undefined {
|
|
906
|
+
if (bossTeamScope.present) {
|
|
907
|
+
return isZhLocale()
|
|
908
|
+
? "Boss 团队模式下不能创建或加入其他团队。"
|
|
909
|
+
: "Boss team-only mode cannot create or join another team.";
|
|
910
|
+
}
|
|
911
|
+
return rejectManagedJoin(classifyMembership(), isZhLocale());
|
|
912
|
+
}
|
|
913
|
+
async function applyCircleScope(ctx: ExtensionContext, scopeId: string, managerSessionId?: string): Promise<void> {
|
|
914
|
+
if (managerSessionId) {
|
|
915
|
+
process.env.AGENT_INTERCOM_MANAGER_TARGET = managerSessionId;
|
|
916
|
+
} else {
|
|
917
|
+
delete process.env.AGENT_INTERCOM_MANAGER_TARGET;
|
|
918
|
+
}
|
|
919
|
+
if (!getLiveContext(ctx)) {
|
|
920
|
+
startSessionRuntime(ctx);
|
|
921
|
+
}
|
|
922
|
+
await switchRuntimeScope(scopeId);
|
|
923
|
+
syncPresenceIdentity(currentSessionId ?? ctx.sessionManager.getSessionId());
|
|
924
|
+
}
|
|
925
|
+
async function createAndJoinNamedTeam(ctx: ExtensionContext, name: string): Promise<string> {
|
|
926
|
+
const blocked = rejectCircleChange();
|
|
927
|
+
if (blocked) throw new Error(blocked);
|
|
928
|
+
const teamName = parseTeamName(name);
|
|
929
|
+
if (!getLiveContext(ctx)) {
|
|
930
|
+
startSessionRuntime(ctx);
|
|
931
|
+
}
|
|
932
|
+
const managerSessionId = currentSessionId ?? ctx.sessionManager.getSessionId();
|
|
933
|
+
const team = createNamedTeam({ name: teamName, managerSessionId });
|
|
934
|
+
await applyCircleScope(ctx, team.scopeId, team.managerSessionId);
|
|
935
|
+
return formatCreateSuccess({ team: team.name, name: currentDisplayName(), zh: isZhLocale() });
|
|
936
|
+
}
|
|
937
|
+
async function listOrJoinCircle(ctx: ExtensionContext, args: string): Promise<string> {
|
|
938
|
+
const zh = isZhLocale();
|
|
939
|
+
const blocked = rejectCircleChange();
|
|
940
|
+
if (blocked) throw new Error(blocked);
|
|
941
|
+
const parsed = parseJoinArgs(args);
|
|
942
|
+
const namedTeams = listNamedTeams();
|
|
943
|
+
const workspaces = await listScopedWorkspaces();
|
|
944
|
+
const circles = buildJoinableCircles({ namedTeams, workspaces });
|
|
945
|
+
if (parsed.kind === "list") {
|
|
946
|
+
return formatJoinableCircleList({ circles, zh });
|
|
947
|
+
}
|
|
948
|
+
let selected = resolveJoinCircle({ parsed, namedTeams, workspaces });
|
|
949
|
+
if (!selected && parsed.kind === "workspace" && parsed.workspace) {
|
|
950
|
+
const scope = await readSessionScope(parsed.workspace);
|
|
951
|
+
if (scope) selected = { name: parsed.workspace, kind: "tmuxdeck", scopeId: scope };
|
|
952
|
+
}
|
|
953
|
+
if (!selected && parsed.kind === "scope" && parsed.scope) {
|
|
954
|
+
const workspace = await workspaceNameForScope(parsed.scope);
|
|
955
|
+
if (workspace) {
|
|
956
|
+
const scope = await readSessionScope(workspace);
|
|
957
|
+
if (scope) selected = { name: workspace, kind: "tmuxdeck", scopeId: scope };
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (!selected) {
|
|
961
|
+
throw new Error(parsed.kind === "index"
|
|
962
|
+
? (zh ? "没有这个编号的团队。" : "No team uses that number.")
|
|
963
|
+
: (zh ? "无法加入该团队。" : "Could not join that team."));
|
|
964
|
+
}
|
|
965
|
+
await applyCircleScope(ctx, selected.scopeId, selected.managerSessionId);
|
|
966
|
+
return selected.kind === "named"
|
|
967
|
+
? formatNamedJoinSuccess({ team: selected.name, name: currentDisplayName(), zh })
|
|
968
|
+
: formatJoinSuccess({ workspace: selected.name, name: currentDisplayName(), zh });
|
|
969
|
+
}
|
|
889
970
|
|
|
890
971
|
function restoreIntercomSessionId(): void {
|
|
891
972
|
if (previousIntercomSessionId === undefined) {
|
|
@@ -1794,6 +1875,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1794
1875
|
"intercom_pending",
|
|
1795
1876
|
"intercom_status",
|
|
1796
1877
|
"intercom_team",
|
|
1878
|
+
"intercom_join",
|
|
1797
1879
|
"contact_supervisor",
|
|
1798
1880
|
]);
|
|
1799
1881
|
|
|
@@ -2707,6 +2789,49 @@ Usage:
|
|
|
2707
2789
|
},
|
|
2708
2790
|
} as any);
|
|
2709
2791
|
|
|
2792
|
+
pi.registerTool({
|
|
2793
|
+
name: "intercom_join",
|
|
2794
|
+
label: "Intercom Join",
|
|
2795
|
+
description: "List, join, or create a named intercom team without tmux. Omit name to list joinable teams. Set create=true to create a team and join as manager.",
|
|
2796
|
+
promptSnippet: "Join or create a named intercom team so intercom_team works without tmux.",
|
|
2797
|
+
promptGuidelines: [
|
|
2798
|
+
"Use intercom_join with no name to list named teams and TmuxDeck workspaces.",
|
|
2799
|
+
"Use intercom_join({ name: \"billing\" }) to join an existing named team or workspace.",
|
|
2800
|
+
"Use intercom_join({ name: \"billing\", create: true }) to create a named team and join as manager.",
|
|
2801
|
+
],
|
|
2802
|
+
parameters: Type.Object({
|
|
2803
|
+
name: Type.Optional(Type.String({ description: "Team name to join. Omit to list joinable teams." })),
|
|
2804
|
+
create: Type.Optional(Type.Boolean({ description: "Create this named team and join as manager. Requires name." })),
|
|
2805
|
+
}),
|
|
2806
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2807
|
+
try {
|
|
2808
|
+
const zh = isZhLocale();
|
|
2809
|
+
if (params.create) {
|
|
2810
|
+
if (typeof params.name !== "string" || !params.name.trim()) {
|
|
2811
|
+
throw new Error(zh ? "创建团队需要名称。" : "Creating a team requires a name.");
|
|
2812
|
+
}
|
|
2813
|
+
const text = await createAndJoinNamedTeam(ctx, params.name);
|
|
2814
|
+
return { content: [{ type: "text", text }] };
|
|
2815
|
+
}
|
|
2816
|
+
const text = await listOrJoinCircle(ctx, typeof params.name === "string" ? params.name : "");
|
|
2817
|
+
return { content: [{ type: "text", text }] };
|
|
2818
|
+
} catch (error) {
|
|
2819
|
+
return { content: [{ type: "text", text: getErrorMessage(error) }], details: toolErrorDetails(error) };
|
|
2820
|
+
}
|
|
2821
|
+
},
|
|
2822
|
+
renderCall(args, theme) {
|
|
2823
|
+
const name = typeof args.name === "string" && args.name.trim() ? args.name.trim() : "";
|
|
2824
|
+
const suffix = args.create === true ? (name ? ` create ${name}` : " create") : (name ? ` ${name}` : "");
|
|
2825
|
+
return new Text(theme.fg("toolTitle", theme.bold(`intercom_join${suffix}`)), 0, 0);
|
|
2826
|
+
},
|
|
2827
|
+
renderResult(result, { isPartial }, theme, context) {
|
|
2828
|
+
if (isPartial) return new Text(theme.fg("warning", "Joining team..."), 0, 0);
|
|
2829
|
+
const details = result.details as { error?: boolean } | undefined;
|
|
2830
|
+
const failed = Boolean(context.isError || details?.error === true);
|
|
2831
|
+
return new Text(`${failed ? theme.fg("error", "✗ ") : theme.fg("success", "✓ ")}${theme.fg(failed ? "error" : "text", firstTextContent(result))}`, 0, 0);
|
|
2832
|
+
},
|
|
2833
|
+
} as any);
|
|
2834
|
+
|
|
2710
2835
|
for (const definition of [
|
|
2711
2836
|
{ name: "intercom_list", label: "Intercom List", action: "list", description: "List active local intercom sessions.", promptSnippet: "List active local intercom sessions." },
|
|
2712
2837
|
{ name: "intercom_pending", label: "Intercom Pending", action: "pending", description: "List unresolved inbound intercom asks with stable IDs, or retrieve one ask's full body. Managers may inspect an owned coworker's inbox. This does not list questions you sent to other sessions.", promptSnippet: "List unresolved inbound asks or retrieve one full ask by its stable ID." },
|
|
@@ -2889,70 +3014,23 @@ Usage:
|
|
|
2889
3014
|
},
|
|
2890
3015
|
});
|
|
2891
3016
|
|
|
3017
|
+
pi.registerCommand("intercom-create", {
|
|
3018
|
+
description: "Create a named intercom team and join it as manager. Does not require tmux.",
|
|
3019
|
+
handler: async (args, ctx) => {
|
|
3020
|
+
try {
|
|
3021
|
+
const name = parseCreateArgs(args);
|
|
3022
|
+
notifyIfLive(ctx, await createAndJoinNamedTeam(ctx, name), "info");
|
|
3023
|
+
} catch (error) {
|
|
3024
|
+
notifyIfLive(ctx, getErrorMessage(error), "error");
|
|
3025
|
+
}
|
|
3026
|
+
},
|
|
3027
|
+
});
|
|
3028
|
+
|
|
2892
3029
|
pi.registerCommand("intercom-join", {
|
|
2893
|
-
description: "Join
|
|
3030
|
+
description: "Join a named intercom team or an existing TmuxDeck workspace circle.",
|
|
2894
3031
|
handler: async (args, ctx) => {
|
|
2895
|
-
const zh = isZhLocale();
|
|
2896
|
-
const fail = (workspace?: string) => {
|
|
2897
|
-
notifyIfLive(ctx, workspace
|
|
2898
|
-
? (zh
|
|
2899
|
-
? `无法加入工作区 ${workspace} 的通话圈。`
|
|
2900
|
-
: `Could not join the intercom circle for workspace ${workspace}.`)
|
|
2901
|
-
: (zh ? "无法加入该工作区通话圈。" : "Could not join that workspace intercom circle."), "error");
|
|
2902
|
-
};
|
|
2903
3032
|
try {
|
|
2904
|
-
|
|
2905
|
-
if (blocked) {
|
|
2906
|
-
notifyIfLive(ctx, blocked, "error");
|
|
2907
|
-
return;
|
|
2908
|
-
}
|
|
2909
|
-
const parsed = parseJoinArgs(args);
|
|
2910
|
-
const workspaces = await listScopedWorkspaces();
|
|
2911
|
-
let workspace: string | undefined;
|
|
2912
|
-
if (parsed.kind === "list") {
|
|
2913
|
-
notifyIfLive(ctx, formatJoinableWorkspaceList({ workspaces, zh }), "info");
|
|
2914
|
-
return;
|
|
2915
|
-
}
|
|
2916
|
-
if (parsed.kind === "index" && parsed.index) {
|
|
2917
|
-
const selected = workspaces[parsed.index - 1];
|
|
2918
|
-
if (!selected) {
|
|
2919
|
-
notifyIfLive(ctx, zh
|
|
2920
|
-
? "没有这个编号的工作区。"
|
|
2921
|
-
: "No workspace uses that number.", "error");
|
|
2922
|
-
return;
|
|
2923
|
-
}
|
|
2924
|
-
workspace = selected.sessionName;
|
|
2925
|
-
} else if (parsed.kind === "workspace" && parsed.workspace) {
|
|
2926
|
-
workspace = parsed.workspace;
|
|
2927
|
-
} else if (parsed.kind === "scope" && parsed.scope) {
|
|
2928
|
-
workspace = await workspaceNameForScope(parsed.scope);
|
|
2929
|
-
if (!workspace) {
|
|
2930
|
-
fail();
|
|
2931
|
-
return;
|
|
2932
|
-
}
|
|
2933
|
-
}
|
|
2934
|
-
if (!workspace) {
|
|
2935
|
-
fail();
|
|
2936
|
-
return;
|
|
2937
|
-
}
|
|
2938
|
-
const scope = await readSessionScope(workspace);
|
|
2939
|
-
if (!scope) {
|
|
2940
|
-
fail(workspace);
|
|
2941
|
-
return;
|
|
2942
|
-
}
|
|
2943
|
-
if (!getLiveContext(ctx)) {
|
|
2944
|
-
startSessionRuntime(ctx);
|
|
2945
|
-
}
|
|
2946
|
-
await switchRuntimeScope(scope);
|
|
2947
|
-
syncPresenceIdentity(currentSessionId ?? ctx.sessionManager.getSessionId());
|
|
2948
|
-
const displayName = currentSessionId
|
|
2949
|
-
? buildPresenceIdentity(pi, currentSessionId).name
|
|
2950
|
-
: (pi.getSessionName()?.trim() || "unnamed");
|
|
2951
|
-
notifyIfLive(ctx, formatJoinSuccess({
|
|
2952
|
-
workspace,
|
|
2953
|
-
name: displayName,
|
|
2954
|
-
zh,
|
|
2955
|
-
}), "info");
|
|
3033
|
+
notifyIfLive(ctx, await listOrJoinCircle(ctx, args), "info");
|
|
2956
3034
|
} catch (error) {
|
|
2957
3035
|
notifyIfLive(ctx, getErrorMessage(error), "error");
|
|
2958
3036
|
}
|
|
@@ -2965,11 +3043,11 @@ Usage:
|
|
|
2965
3043
|
const zh = isZhLocale();
|
|
2966
3044
|
try {
|
|
2967
3045
|
const membership = classifyMembership();
|
|
2968
|
-
const
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3046
|
+
const namedTeam = runtimeScopeId ? findNamedTeamByScope(runtimeScopeId) : undefined;
|
|
3047
|
+
const workspace = namedTeam?.name
|
|
3048
|
+
? undefined
|
|
3049
|
+
: await currentTmuxWorkspace()
|
|
3050
|
+
?? (runtimeScopeId ? await workspaceNameForScope(runtimeScopeId) : undefined);
|
|
2973
3051
|
let peers: string[] = [];
|
|
2974
3052
|
if (client?.isConnected()) {
|
|
2975
3053
|
const sessions = await client.listSessions();
|
|
@@ -2980,7 +3058,8 @@ Usage:
|
|
|
2980
3058
|
notifyIfLive(ctx, formatJoinStatus({
|
|
2981
3059
|
membership,
|
|
2982
3060
|
workspace,
|
|
2983
|
-
|
|
3061
|
+
team: namedTeam?.name,
|
|
3062
|
+
name: currentDisplayName(),
|
|
2984
3063
|
peers,
|
|
2985
3064
|
zh,
|
|
2986
3065
|
}), "info");
|
package/named-teams.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ensureIntercomRuntimeDir, getAgentDirPath, getIntercomDirPath } from "./broker/paths.ts";
|
|
5
|
+
import { writeDurableJson } from "./durable-json.ts";
|
|
6
|
+
import { isTmuxDeckScope, isZhLocale, type ParsedJoinArgs, type ScopedWorkspace } from "./workspace-join.ts";
|
|
7
|
+
|
|
8
|
+
export const NAMED_TEAMS_FILE = "named-teams.json";
|
|
9
|
+
export const NAMED_TEAMS_VERSION = 1;
|
|
10
|
+
export const NAMED_TEAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
11
|
+
|
|
12
|
+
export interface NamedTeam {
|
|
13
|
+
name: string;
|
|
14
|
+
scopeId: string;
|
|
15
|
+
managerSessionId: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type JoinableCircleKind = "named" | "tmuxdeck";
|
|
20
|
+
|
|
21
|
+
export interface JoinableCircle {
|
|
22
|
+
name: string;
|
|
23
|
+
kind: JoinableCircleKind;
|
|
24
|
+
scopeId: string;
|
|
25
|
+
managerSessionId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface NamedTeamsFile {
|
|
29
|
+
version: number;
|
|
30
|
+
teams: NamedTeam[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function teamsFilePath(agentDir?: string): string {
|
|
34
|
+
return join(getIntercomDirPath(agentDir ?? getAgentDirPath()), NAMED_TEAMS_FILE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function genericReadError(zh: boolean): Error {
|
|
38
|
+
return new Error(zh ? "无法读取本地团队列表。" : "Could not read the local named-team list.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function genericWriteError(zh: boolean): Error {
|
|
42
|
+
return new Error(zh ? "无法创建该团队。" : "Could not create that named team.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseStoredTeam(value: unknown): NamedTeam | undefined {
|
|
50
|
+
if (!isPlainObject(value)) return undefined;
|
|
51
|
+
if (typeof value.name !== "string" || !NAMED_TEAM_NAME_PATTERN.test(value.name)) return undefined;
|
|
52
|
+
if (typeof value.scopeId !== "string" || !isTmuxDeckScope(value.scopeId)) return undefined;
|
|
53
|
+
if (typeof value.managerSessionId !== "string" || !value.managerSessionId.trim()) return undefined;
|
|
54
|
+
if (value.managerSessionId !== value.managerSessionId.trim() || /[\u0000-\u001f\u007f]/.test(value.managerSessionId)) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
name: value.name,
|
|
62
|
+
scopeId: value.scopeId,
|
|
63
|
+
managerSessionId: value.managerSessionId,
|
|
64
|
+
createdAt: value.createdAt,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function parseTeamName(raw: string, zh: boolean = isZhLocale()): string {
|
|
69
|
+
const name = raw.trim();
|
|
70
|
+
if (!name || name.includes(" ") || !NAMED_TEAM_NAME_PATTERN.test(name)) {
|
|
71
|
+
throw new Error(zh
|
|
72
|
+
? "用法:/intercom-create <名称>\n团队名需以字母开头,只能包含字母、数字、连字符或下划线(最长 32)。"
|
|
73
|
+
: "Usage: /intercom-create <name>\nTeam names start with a letter and may include letters, numbers, hyphens, or underscores (max 32).");
|
|
74
|
+
}
|
|
75
|
+
return name;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseCreateArgs(raw: string, zh: boolean = isZhLocale()): string {
|
|
79
|
+
const args = raw.trim().split(/\s+/).filter(Boolean);
|
|
80
|
+
if (args.length !== 1) {
|
|
81
|
+
throw new Error(zh
|
|
82
|
+
? "用法:/intercom-create <名称>"
|
|
83
|
+
: "Usage: /intercom-create <name>");
|
|
84
|
+
}
|
|
85
|
+
return parseTeamName(args[0]!, zh);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function generateNamedTeamScope(existing: Iterable<string> = []): string {
|
|
89
|
+
const taken = new Set(existing);
|
|
90
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
91
|
+
const scopeId = randomBytes(24).toString("hex");
|
|
92
|
+
if (!taken.has(scopeId)) return scopeId;
|
|
93
|
+
}
|
|
94
|
+
throw genericWriteError(isZhLocale());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function listNamedTeams(agentDir?: string): NamedTeam[] {
|
|
98
|
+
const zh = isZhLocale();
|
|
99
|
+
let raw: string;
|
|
100
|
+
try {
|
|
101
|
+
raw = readFileSync(teamsFilePath(agentDir), "utf8");
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
throw genericReadError(zh);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let parsed: unknown;
|
|
110
|
+
try {
|
|
111
|
+
parsed = JSON.parse(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
throw genericReadError(zh);
|
|
114
|
+
}
|
|
115
|
+
if (!isPlainObject(parsed) || parsed.version !== NAMED_TEAMS_VERSION || !Array.isArray(parsed.teams)) {
|
|
116
|
+
throw genericReadError(zh);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const teams: NamedTeam[] = [];
|
|
120
|
+
const names = new Set<string>();
|
|
121
|
+
const scopes = new Set<string>();
|
|
122
|
+
for (const entry of parsed.teams) {
|
|
123
|
+
const team = parseStoredTeam(entry);
|
|
124
|
+
if (!team || names.has(team.name) || scopes.has(team.scopeId)) {
|
|
125
|
+
throw genericReadError(zh);
|
|
126
|
+
}
|
|
127
|
+
names.add(team.name);
|
|
128
|
+
scopes.add(team.scopeId);
|
|
129
|
+
teams.push(team);
|
|
130
|
+
}
|
|
131
|
+
return teams;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function findNamedTeam(name: string, agentDir?: string): NamedTeam | undefined {
|
|
135
|
+
return listNamedTeams(agentDir).find((team) => team.name === name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function findNamedTeamByScope(scopeId: string, agentDir?: string): NamedTeam | undefined {
|
|
139
|
+
return listNamedTeams(agentDir).find((team) => team.scopeId === scopeId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function createNamedTeam(input: {
|
|
143
|
+
name: string;
|
|
144
|
+
managerSessionId: string;
|
|
145
|
+
agentDir?: string;
|
|
146
|
+
now?: number;
|
|
147
|
+
generateScope?: () => string;
|
|
148
|
+
}): NamedTeam {
|
|
149
|
+
const zh = isZhLocale();
|
|
150
|
+
const name = parseTeamName(input.name, zh);
|
|
151
|
+
const managerSessionId = input.managerSessionId.trim();
|
|
152
|
+
if (!managerSessionId || managerSessionId !== input.managerSessionId || /[\u0000-\u001f\u007f]/.test(managerSessionId)) {
|
|
153
|
+
throw genericWriteError(zh);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const existing = listNamedTeams(input.agentDir);
|
|
157
|
+
if (existing.some((team) => team.name === name)) {
|
|
158
|
+
throw new Error(zh
|
|
159
|
+
? `团队 ${name} 已存在。`
|
|
160
|
+
: `A named team called ${name} already exists.`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const scopeId = (input.generateScope ?? (() => generateNamedTeamScope(existing.map((team) => team.scopeId))))();
|
|
164
|
+
if (!isTmuxDeckScope(scopeId) || existing.some((team) => team.scopeId === scopeId)) {
|
|
165
|
+
throw genericWriteError(zh);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const team: NamedTeam = {
|
|
169
|
+
name,
|
|
170
|
+
scopeId,
|
|
171
|
+
managerSessionId,
|
|
172
|
+
createdAt: input.now ?? Date.now(),
|
|
173
|
+
};
|
|
174
|
+
const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
|
|
175
|
+
ensureIntercomRuntimeDir(dir);
|
|
176
|
+
const payload: NamedTeamsFile = { version: NAMED_TEAMS_VERSION, teams: [...existing, team] };
|
|
177
|
+
writeDurableJson(teamsFilePath(input.agentDir), payload);
|
|
178
|
+
return team;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildJoinableCircles(input: {
|
|
182
|
+
namedTeams: NamedTeam[];
|
|
183
|
+
workspaces: ScopedWorkspace[];
|
|
184
|
+
}): JoinableCircle[] {
|
|
185
|
+
return [
|
|
186
|
+
...input.namedTeams.map((team) => ({
|
|
187
|
+
name: team.name,
|
|
188
|
+
kind: "named" as const,
|
|
189
|
+
scopeId: team.scopeId,
|
|
190
|
+
managerSessionId: team.managerSessionId,
|
|
191
|
+
})),
|
|
192
|
+
...input.workspaces.map((workspace) => ({
|
|
193
|
+
name: workspace.sessionName,
|
|
194
|
+
kind: "tmuxdeck" as const,
|
|
195
|
+
scopeId: workspace.scopeId,
|
|
196
|
+
})),
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function resolveJoinCircle(input: {
|
|
201
|
+
parsed: ParsedJoinArgs;
|
|
202
|
+
namedTeams: NamedTeam[];
|
|
203
|
+
workspaces: ScopedWorkspace[];
|
|
204
|
+
}): JoinableCircle | undefined {
|
|
205
|
+
const circles = buildJoinableCircles(input);
|
|
206
|
+
if (input.parsed.kind === "list") return undefined;
|
|
207
|
+
if (input.parsed.kind === "index") {
|
|
208
|
+
return input.parsed.index ? circles[input.parsed.index - 1] : undefined;
|
|
209
|
+
}
|
|
210
|
+
if (input.parsed.kind === "workspace" && input.parsed.workspace) {
|
|
211
|
+
return circles.find((circle) => circle.name === input.parsed.workspace);
|
|
212
|
+
}
|
|
213
|
+
if (input.parsed.kind === "scope" && input.parsed.scope) {
|
|
214
|
+
return circles.find((circle) => circle.scopeId === input.parsed.scope);
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function formatJoinableCircleList(input: {
|
|
220
|
+
circles: Array<Pick<JoinableCircle, "name" | "kind">>;
|
|
221
|
+
zh: boolean;
|
|
222
|
+
}): string {
|
|
223
|
+
if (input.circles.length === 0) {
|
|
224
|
+
return input.zh
|
|
225
|
+
? "没有可加入的团队。\n使用 /intercom-create <名称> 创建一个。"
|
|
226
|
+
: "No joinable teams found.\nCreate one with /intercom-create <name>.";
|
|
227
|
+
}
|
|
228
|
+
const header = input.zh ? "可加入的通话团队:" : "Joinable intercom teams:";
|
|
229
|
+
const footer = input.zh ? "请输入编号或精确名称:" : "Select a team by number or exact name:";
|
|
230
|
+
return [
|
|
231
|
+
header,
|
|
232
|
+
...input.circles.map((circle, index) => {
|
|
233
|
+
const suffix = circle.kind === "tmuxdeck" ? " (TmuxDeck)" : "";
|
|
234
|
+
return ` ${index + 1}) ${circle.name}${suffix}`;
|
|
235
|
+
}),
|
|
236
|
+
footer,
|
|
237
|
+
].join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function formatCreateSuccess(input: { team: string; name: string; zh: boolean }): string {
|
|
241
|
+
return input.zh
|
|
242
|
+
? `已创建团队 ${input.team},并以管理者身份加入。\n你的显示名:${input.name}`
|
|
243
|
+
: `Created team ${input.team} and joined as manager.\nDisplay name: ${input.name}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function formatNamedJoinSuccess(input: { team: string; name: string; zh: boolean }): string {
|
|
247
|
+
return input.zh
|
|
248
|
+
? `已加入团队 ${input.team}。\n身份:团队成员\n你的显示名:${input.name}`
|
|
249
|
+
: `Joined team ${input.team}.\nRole: teammate\nDisplay name: ${input.name}`;
|
|
250
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctliz/agent-intercom-pi",
|
|
3
|
-
"version": "0.12.0-connect.
|
|
3
|
+
"version": "0.12.0-connect.9",
|
|
4
4
|
"description": "Pi coding-agent intercom for local messaging with Codex, Claude Code, OpenCode, Grok Build, and AGY agents.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -78,6 +78,16 @@ Do not add artificial sleeps to form a batch. Each original message keeps its se
|
|
|
78
78
|
|
|
79
79
|
### Pattern 3: Find Your Manager or Team
|
|
80
80
|
|
|
81
|
+
If no team exists yet, create one and have the other session join. This does not require tmux:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
intercom_join({ name: "billing", create: true }) // manager
|
|
85
|
+
intercom_join({ name: "billing" }) // other session
|
|
86
|
+
intercom_team({})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`/intercom-create billing` and `/intercom-join billing` do the same from the command line. `intercom_join({})` lists named teams and TmuxDeck workspaces without printing raw scopes.
|
|
90
|
+
|
|
81
91
|
Orchestrator-owned coworkers should use the no-argument team tool instead of searching the global peer list:
|
|
82
92
|
|
|
83
93
|
```typescript
|
|
@@ -210,7 +220,7 @@ intercom_reply({ askId: "ask-...", message: "Use the v2 API." })
|
|
|
210
220
|
|
|
211
221
|
**Important:** Only sessions where `pi-subagents` supplied child bridge metadata
|
|
212
222
|
get the `contact_supervisor` tool. Normal sessions use the split `intercom_send`,
|
|
213
|
-
`intercom_ask`, `intercom_reply`, `intercom_team`, `intercom_list`, `intercom_pending`, and
|
|
223
|
+
`intercom_ask`, `intercom_reply`, `intercom_team`, `intercom_join`, `intercom_list`, `intercom_pending`, and
|
|
214
224
|
`intercom_status` tools. If you see the formatted supervisor decision/progress update message, treat
|
|
215
225
|
it as a `contact_supervisor` escalation.
|
|
216
226
|
|
|
@@ -222,6 +232,7 @@ it as a `contact_supervisor` escalation.
|
|
|
222
232
|
| `ask` | Waits up to 30 seconds, then continues asynchronously | You need an answer but should not hold the agent indefinitely |
|
|
223
233
|
| `reply` | Responds to the active or pending inbound ask | You were asked something and need to answer naturally |
|
|
224
234
|
| `pending` | Lists unresolved inbound asks | You need to see who is waiting before replying |
|
|
235
|
+
| `join` | Lists, joins, or creates a named team | You need a team without tmux |
|
|
225
236
|
| `list` | Returns all sessions with live status | You need to discover targets or choose an idle peer |
|
|
226
237
|
| `status` | Returns your connection state | Troubleshooting |
|
|
227
238
|
|
|
@@ -397,14 +408,15 @@ Use `/name` so others can target you easily:
|
|
|
397
408
|
/name planner
|
|
398
409
|
```
|
|
399
410
|
|
|
400
|
-
To
|
|
411
|
+
To form a team without tmux:
|
|
401
412
|
|
|
402
413
|
```
|
|
403
|
-
/intercom-
|
|
414
|
+
/intercom-create billing
|
|
415
|
+
/intercom-join billing
|
|
404
416
|
/intercom-status
|
|
405
417
|
```
|
|
406
418
|
|
|
407
|
-
`/intercom-join` with no argument lists
|
|
419
|
+
Agents can use `intercom_join({ name: "billing", create: true })` or `intercom_join({ name: "billing" })`. `/intercom-join` with no argument lists named teams and TmuxDeck workspaces. Joining a TmuxDeck workspace is same-scope messaging only; it does not enroll you as a Team Worker.
|
|
408
420
|
|
|
409
421
|
## Error Handling
|
|
410
422
|
|
package/workspace-join.ts
CHANGED
|
@@ -50,8 +50,8 @@ export function parseJoinArgs(raw: string): ParsedJoinArgs {
|
|
|
50
50
|
}
|
|
51
51
|
if (args.length !== 1 || args[0].startsWith("-")) {
|
|
52
52
|
throw new Error(isZhLocale()
|
|
53
|
-
? "用法:/intercom-join [
|
|
54
|
-
: "Usage: /intercom-join [
|
|
53
|
+
? "用法:/intercom-join [团队名或编号] 或 /intercom-join --scope <48 hex>"
|
|
54
|
+
: "Usage: /intercom-join [team or number] or /intercom-join --scope <48 hex>");
|
|
55
55
|
}
|
|
56
56
|
if (/^[1-9]\d*$/.test(args[0])) {
|
|
57
57
|
return { kind: "index", index: Number(args[0]) };
|
|
@@ -206,20 +206,24 @@ export function formatJoinSuccess(input: { workspace: string; name: string; zh:
|
|
|
206
206
|
export function formatJoinStatus(input: {
|
|
207
207
|
membership: JoinMembership;
|
|
208
208
|
workspace?: string;
|
|
209
|
+
team?: string;
|
|
209
210
|
name: string;
|
|
210
211
|
peers: string[];
|
|
211
212
|
zh: boolean;
|
|
212
213
|
}): string {
|
|
214
|
+
const circle = input.zh
|
|
215
|
+
? (input.team ? `团队:${input.team}` : `工作区:${input.workspace || "无"}`)
|
|
216
|
+
: (input.team ? `Team: ${input.team}` : `Workspace: ${input.workspace || "none"}`);
|
|
213
217
|
return input.zh
|
|
214
218
|
? [
|
|
215
219
|
input.membership,
|
|
216
|
-
|
|
220
|
+
circle,
|
|
217
221
|
`显示名:${input.name}`,
|
|
218
222
|
`同圈可见:${input.peers.length > 0 ? input.peers.join(", ") : "无"}`,
|
|
219
223
|
].join("\n")
|
|
220
224
|
: [
|
|
221
225
|
input.membership,
|
|
222
|
-
|
|
226
|
+
circle,
|
|
223
227
|
`Display name: ${input.name}`,
|
|
224
228
|
`Visible in circle: ${input.peers.length > 0 ? input.peers.join(", ") : "none"}`,
|
|
225
229
|
].join("\n");
|