viber-channel 0.8.0 → 0.8.1

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.
@@ -17,7 +17,7 @@
17
17
  * MCP tool-result shape. It does NOT hold state.
18
18
  */
19
19
  import { postMessage, parseArtifact, type Artifact } from "./messages.js";
20
- import { listPeers, openDm, type OpenDmResult } from "./peers.js";
20
+ import { listPeersAuto, openDm, type OpenDmResult } from "./peers.js";
21
21
  import { ConversationTokenExpiredError } from "./messages.js";
22
22
 
23
23
  /** MCP tool-result shape (text content + optional error flag). */
@@ -66,22 +66,36 @@ function errorText(s: string): AgentToolResult {
66
66
  }
67
67
 
68
68
  /**
69
- * list_agents — return this project's other agents so the model can pick one to
70
- * message. Guards the startup window: an empty instance token means "channel not
71
- * ready" rather than hitting the peers endpoint with no credential.
69
+ * list_agents — return the other agents so the model can pick one to message.
70
+ * Guards the startup window: an empty instance token means "channel not ready"
71
+ * rather than hitting the peers endpoint with no credential.
72
+ *
73
+ * #307: the listing runs at the widest allowed scope (listPeersAuto) — an
74
+ * ORCHESTRATOR instance sees the agents of ALL its owner's projects (each
75
+ * entry then carries a `project` field); a regular instance transparently
76
+ * falls back to its own project (unchanged behaviour, no `project` field).
72
77
  */
73
78
  export async function listAgents(ctx: AgentToolsContext): Promise<AgentToolResult> {
74
79
  if (!ctx.instanceToken()) {
75
80
  return errorText("Channel not ready: no instance identity yet.");
76
81
  }
77
82
  try {
78
- const peers = await listPeers(ctx.baseUrl(), ctx.instanceToken());
83
+ const { peers, scope } = await listPeersAuto(ctx.baseUrl(), ctx.instanceToken());
79
84
  // Project only what the model needs to pick a peer (drop last_seen /
80
85
  // active_conversation_id — available over the wire if a future tool needs them).
81
- const summary = peers.map((p) => ({ id: p.id, label: p.label, kind: p.kind, online: p.online }));
86
+ const summary = peers.map((p) => ({
87
+ id: p.id,
88
+ label: p.label,
89
+ kind: p.kind,
90
+ online: p.online,
91
+ // Present only in the user-scoped (orchestrator) listing.
92
+ ...(p.project_name !== undefined ? { project: p.project_name } : {}),
93
+ }));
82
94
  const body =
83
95
  summary.length === 0
84
- ? "No other agents are currently registered in this project."
96
+ ? scope === "user"
97
+ ? "No other agents are currently registered in any of your projects."
98
+ : "No other agents are currently registered in this project."
85
99
  : JSON.stringify(summary, null, 2);
86
100
  return text(body);
87
101
  } catch (err) {
@@ -68,16 +68,20 @@ const TOOL_DEFS = [
68
68
  {
69
69
  name: "list_agents",
70
70
  description:
71
- "List the OTHER agents (instances) in this project you can DM. Returns each agent's id, " +
72
- "label, runtime kind, and whether it is online. Use it to find an id before message_agent.",
71
+ "List the OTHER agents (instances) you can DM. Returns each agent's id, " +
72
+ "label, runtime kind, and whether it is online. Use it to find an id before message_agent. " +
73
+ "Normally project-scoped; an ORCHESTRATOR instance (#307) sees all the owner's projects, " +
74
+ "each entry tagged with a `project` field.",
73
75
  inputSchema: { type: "object", properties: {}, additionalProperties: false },
74
76
  },
75
77
  {
76
78
  name: "message_agent",
77
79
  description:
78
- "Send a direct message to another agent in this project. Pass the target agent's instance id " +
80
+ "Send a direct message to another agent. Pass the target agent's instance id " +
79
81
  "(from list_agents) and the text. Opens or reuses a private 1:1 DM and posts your message; " +
80
- "the agent's reply arrives back on this channel. Fails if the target agent is offline.",
82
+ "the agent's reply arrives back on this channel. Targets are normally same-project; an " +
83
+ "ORCHESTRATOR instance (#307) can also message agents of the owner's other projects. " +
84
+ "Fails if the target agent is offline.",
81
85
  inputSchema: {
82
86
  type: "object",
83
87
  properties: {
@@ -26,15 +26,25 @@ export function capabilitiesText(): string {
26
26
  "",
27
27
  "Spawn one agent (CLI, non-interactive):",
28
28
  " vibe-master spawn --permission <read-only|read-write> \\",
29
- " [--runtime codex|claude|gemma] [--name <id>]",
29
+ " [--runtime codex|claude|gemma] [--name <id>] [--scope <name>]",
30
30
  " → only --permission is required; --runtime and --name are optional",
31
31
  " (--name auto-generated if omitted). Prints the agent id; it registers",
32
32
  " in Viber and comes online.",
33
33
  "",
34
+ "Multi-scope (#307): spawn into ANOTHER project by name. Register a project",
35
+ "folder once with `vibe-master scopes add <name> <path>`, then",
36
+ "`vibe-master spawn --scope <name> …` lands the agent in that project (its own",
37
+ ".viber auth). `--scope` and `--cwd` are exclusive. `vibe-master scopes` lists them.",
38
+ "",
34
39
  "Interactive: `vibe-master tui` (menu: + Add agent → runtime → permission → name).",
35
40
  "Inspect / stop: `vibe-master list` · `vibe-master kill <id>`.",
36
41
  "",
37
42
  "Talk to a spawned agent from here with list_agents + message_agent.",
43
+ "Cross-project (#307): by default list_agents/message_agent see only THIS",
44
+ "project. If the OWNER marks this instance an ORCHESTRATOR (toggle on the web",
45
+ "project page — server-authorized, never self-declared), list_agents then covers",
46
+ "ALL the owner's projects (each entry tagged with its project) and message_agent",
47
+ "can DM an agent in another of those projects.",
38
48
  "Requires vibe-master installed on the machine (see the Install page on the web).",
39
49
  "Run `vibe-master --help` for the full command reference.",
40
50
  ].join("\n");
package/lib/peers.ts CHANGED
@@ -11,7 +11,10 @@
11
11
 
12
12
  import { cfAccessHeaders } from "./cfAccess.js";
13
13
 
14
- /** A peer agent as returned by GET /api/instances/peers. */
14
+ /** A peer agent as returned by GET /api/instances/peers.
15
+ * project_id/project_name are present only in the user-scoped listing (#307):
16
+ * an orchestrator sees peers across all its owner's projects, each tagged
17
+ * with its project. */
15
18
  export interface PeerView {
16
19
  id: string;
17
20
  label: string | null;
@@ -19,17 +22,35 @@ export interface PeerView {
19
22
  online: boolean;
20
23
  last_seen: number | null;
21
24
  active_conversation_id: string | null;
25
+ project_id?: number;
26
+ project_name?: string;
27
+ }
28
+
29
+ /** Error carrying the HTTP status so callers can branch on 403 (#307). */
30
+ export class PeersHttpError extends Error {
31
+ constructor(
32
+ message: string,
33
+ public readonly status: number,
34
+ ) {
35
+ super(message);
36
+ this.name = "PeersHttpError";
37
+ }
22
38
  }
23
39
 
24
40
  /**
25
41
  * List the OTHER active instances of this agent's project (self excluded).
26
- * Throws on a non-2xx response so the caller can surface a clear tool error.
42
+ * With scope "user" (#307), an ORCHESTRATOR instance lists the peers of ALL
43
+ * its owner's projects (the server returns 403 for a non-orchestrator).
44
+ * Throws PeersHttpError on a non-2xx response so the caller can surface a
45
+ * clear tool error — or fall back from user scope on a 403.
27
46
  */
28
47
  export async function listPeers(
29
48
  baseUrl: string,
30
49
  instanceToken: string,
50
+ scope?: "user",
31
51
  ): Promise<PeerView[]> {
32
- const resp = await fetch(`${baseUrl}/api/instances/peers`, {
52
+ const qs = scope === "user" ? "?scope=user" : "";
53
+ const resp = await fetch(`${baseUrl}/api/instances/peers${qs}`, {
33
54
  method: "GET",
34
55
  headers: { Authorization: `Bearer ${instanceToken}`, ...cfAccessHeaders() },
35
56
  });
@@ -41,12 +62,33 @@ export async function listPeers(
41
62
  } catch {
42
63
  /* non-JSON body */
43
64
  }
44
- throw new Error(`listPeers failed: ${detail}`);
65
+ throw new PeersHttpError(`listPeers failed: ${detail}`, resp.status);
45
66
  }
46
67
  const body = (await resp.json()) as { instances?: PeerView[] };
47
68
  return Array.isArray(body.instances) ? body.instances : [];
48
69
  }
49
70
 
71
+ /**
72
+ * List peers at the WIDEST scope this instance is allowed (#307): try the
73
+ * user-scoped listing first; a 403 (non-orchestrator) transparently falls
74
+ * back to the project-scoped listing. Any other failure propagates. Single
75
+ * tool surface, zero client-side configuration: orchestrators see all their
76
+ * owner's projects, workers keep seeing their own project only.
77
+ */
78
+ export async function listPeersAuto(
79
+ baseUrl: string,
80
+ instanceToken: string,
81
+ ): Promise<{ peers: PeerView[]; scope: "user" | "project" }> {
82
+ try {
83
+ return { peers: await listPeers(baseUrl, instanceToken, "user"), scope: "user" };
84
+ } catch (err) {
85
+ if (err instanceof PeersHttpError && err.status === 403) {
86
+ return { peers: await listPeers(baseUrl, instanceToken), scope: "project" };
87
+ }
88
+ throw err;
89
+ }
90
+ }
91
+
50
92
  /** Result of opening (or reusing) a DM with a peer — carries the caller's own
51
93
  * membership conversation_token so it can post into the DM. */
52
94
  export interface OpenDmResult {
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "viber-channel",
3
- "version": "0.8.0",
4
- "description": "Voice + text MCP channel between a Claude Code session and the Viber UI (https://viber.dgypx.dev). Push transcripts to Claude; send_message tool delivers text back to the UI.",
5
- "type": "module",
6
- "bin": {
7
- "viber-channel": "./viber-channel.ts",
8
- "viber-codex-bridge": "./viber-codex-bridge.ts",
9
- "viber-gemma-bridge": "./viber-gemma-bridge.ts",
10
- "viber-codex-supervisor": "./viber-codex-supervisor.ts"
11
- },
12
- "files": [
13
- "viber-channel.ts",
14
- "viber-codex-bridge.ts",
15
- "viber-gemma-bridge.ts",
16
- "viber-codex-supervisor.ts",
17
- "lib/",
18
- "README.md"
19
- ],
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/dgx80/viber.git",
23
- "directory": "viber-channel"
24
- },
25
- "homepage": "https://viber.dgypx.dev",
26
- "bugs": {
27
- "url": "https://github.com/dgx80/viber/issues"
28
- },
29
- "keywords": [
30
- "viber",
31
- "claude-code",
32
- "mcp",
33
- "channel",
34
- "voice",
35
- "transcription"
36
- ],
37
- "license": "MIT",
38
- "scripts": {
39
- "start": "bun run viber-channel.ts",
40
- "start:codex-bridge": "bun run viber-codex-bridge.ts",
41
- "test": "bun test"
42
- },
43
- "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.0.0"
45
- }
46
- }
1
+ {
2
+ "name": "viber-channel",
3
+ "version": "0.8.1",
4
+ "description": "Voice + text MCP channel between a Claude Code session and the Viber UI (https://viber.dgypx.dev). Push transcripts to Claude; send_message tool delivers text back to the UI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "viber-channel": "./viber-channel.ts",
8
+ "viber-codex-bridge": "./viber-codex-bridge.ts",
9
+ "viber-gemma-bridge": "./viber-gemma-bridge.ts",
10
+ "viber-codex-supervisor": "./viber-codex-supervisor.ts"
11
+ },
12
+ "files": [
13
+ "viber-channel.ts",
14
+ "viber-codex-bridge.ts",
15
+ "viber-gemma-bridge.ts",
16
+ "viber-codex-supervisor.ts",
17
+ "lib/",
18
+ "README.md"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/dgx80/viber.git",
23
+ "directory": "viber-channel"
24
+ },
25
+ "homepage": "https://viber.dgypx.dev",
26
+ "bugs": {
27
+ "url": "https://github.com/dgx80/viber/issues"
28
+ },
29
+ "keywords": [
30
+ "viber",
31
+ "claude-code",
32
+ "mcp",
33
+ "channel",
34
+ "voice",
35
+ "transcription"
36
+ ],
37
+ "license": "MIT",
38
+ "scripts": {
39
+ "start": "bun run viber-channel.ts",
40
+ "start:codex-bridge": "bun run viber-codex-bridge.ts",
41
+ "test": "bun test"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.0.0"
45
+ }
46
+ }
package/viber-channel.ts CHANGED
@@ -428,10 +428,12 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
428
428
  {
429
429
  name: "list_agents",
430
430
  description:
431
- "List the OTHER agents (instances) in this project you can talk to directly (#288). " +
431
+ "List the OTHER agents (instances) you can talk to directly (#288). " +
432
432
  "Returns each agent's id, label, runtime kind, and whether it is currently online. " +
433
433
  "Use this to find the id of an agent (e.g. 'Codex Review') before calling message_agent. " +
434
- "Only agents in the same project are returned; you are never in the list.",
434
+ "Normally scoped to this project; if the owner designated this instance an ORCHESTRATOR " +
435
+ "(#307), the list covers ALL the owner's projects and each entry carries a `project` field. " +
436
+ "You are never in the list.",
435
437
  inputSchema: {
436
438
  type: "object" as const,
437
439
  properties: {},
@@ -442,10 +444,12 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
442
444
  {
443
445
  name: "message_agent",
444
446
  description:
445
- "Send a direct message to another agent in this project (#288). " +
447
+ "Send a direct message to another agent (#288). " +
446
448
  "Pass the target agent's instance id (from list_agents, or the from_instance_id of a DM you received) " +
447
449
  "and the message text. Opens or reuses a private 1:1 DM with that agent and posts your message; " +
448
450
  "the agent's reply arrives back on this channel tagged source=agent-dm. " +
451
+ "Targets are normally same-project; an ORCHESTRATOR instance (#307) can also message agents " +
452
+ "of the owner's other projects (ids from its cross-project list_agents). " +
449
453
  "Fails if the target agent is offline.",
450
454
  inputSchema: {
451
455
  type: "object" as const,