viber-channel 0.7.2 → 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.
- package/lib/agent_tools.ts +21 -7
- package/lib/bridge_core.ts +13 -4
- package/lib/bridge_tool_host.ts +8 -4
- package/lib/capabilities.ts +51 -0
- package/lib/lockfile.ts +2 -2
- package/lib/peers.ts +46 -4
- package/lib/version_check.ts +124 -0
- package/package.json +46 -46
- package/viber-channel.ts +67 -17
package/lib/agent_tools.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
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) => ({
|
|
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
|
-
?
|
|
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) {
|
package/lib/bridge_core.ts
CHANGED
|
@@ -841,14 +841,23 @@ export function buildAgentIdentityInstructions(opts: {
|
|
|
841
841
|
|
|
842
842
|
/**
|
|
843
843
|
* Read the agent identity from the spawn env and build the identity line.
|
|
844
|
-
* `VIBER_AGENT_NAME_EXPLICIT === "1"` gates whether
|
|
845
|
-
*
|
|
846
|
-
*
|
|
844
|
+
* `VIBER_AGENT_NAME_EXPLICIT === "1"` gates whether the spawn label is treated as
|
|
845
|
+
* an explicit name (the label is always set — even for auto ids — so the separate
|
|
846
|
+
* flag is the only safe signal). Shared by every runtime:
|
|
847
|
+
* - the codex/gemma bridges set `VIBER_CODEX_BRIDGE_LABEL`;
|
|
848
|
+
* - a Claude terminal session (#328) has no bridge — its label is
|
|
849
|
+
* `VIBER_CHANNEL_LABEL` (set by buildClaudeLaunchSpec) — so fall back to it.
|
|
847
850
|
*/
|
|
848
851
|
export function agentIdentityFromEnv(): string {
|
|
849
852
|
const explicit = process.env.VIBER_AGENT_NAME_EXPLICIT === "1";
|
|
853
|
+
// A BLANK bridge label counts as absent (defense in depth, #328): the terminal
|
|
854
|
+
// launcher wipes an inherited VIBER_CODEX_BRIDGE_LABEL to "", so a Claude
|
|
855
|
+
// session falls through to its own VIBER_CHANNEL_LABEL instead of a leaked
|
|
856
|
+
// parent bridge name.
|
|
857
|
+
const bridgeLabel = process.env.VIBER_CODEX_BRIDGE_LABEL?.trim();
|
|
858
|
+
const label = bridgeLabel || process.env.VIBER_CHANNEL_LABEL;
|
|
850
859
|
return buildAgentIdentityInstructions({
|
|
851
|
-
explicitName: explicit ?
|
|
860
|
+
explicitName: explicit ? label : undefined,
|
|
852
861
|
role: process.env.VIBER_AGENT_ROLE,
|
|
853
862
|
});
|
|
854
863
|
}
|
package/lib/bridge_tool_host.ts
CHANGED
|
@@ -68,16 +68,20 @@ const TOOL_DEFS = [
|
|
|
68
68
|
{
|
|
69
69
|
name: "list_agents",
|
|
70
70
|
description:
|
|
71
|
-
"List the OTHER agents (instances)
|
|
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
|
|
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.
|
|
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: {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control-plane capabilities help (#332).
|
|
3
|
+
*
|
|
4
|
+
* An agent connected via the channel does not otherwise know that a `vibe-master`
|
|
5
|
+
* control plane exists on the machine, nor how to spawn other agents with it. Per
|
|
6
|
+
* the issue + JP: this must be DISCOVERABLE ON DEMAND, never injected into every
|
|
7
|
+
* conversation. So the channel exposes a `capabilities` MCP tool that returns
|
|
8
|
+
* this text only when the agent chooses to call it; the channel instructions
|
|
9
|
+
* carry just a one-line pointer to the tool, not its contents.
|
|
10
|
+
*
|
|
11
|
+
* Pure (no I/O) so it is trivially testable and identical across hosts.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export function capabilitiesText(): string {
|
|
15
|
+
return [
|
|
16
|
+
"Viber control plane (vibe-master) — orchestrate other agents from this machine.",
|
|
17
|
+
"",
|
|
18
|
+
"vibe-master is a CLI + TUI (installed on the operator machine) that spawns and",
|
|
19
|
+
"supervises agents which appear in the Viber web UI and answer in their own",
|
|
20
|
+
"conversations. Use it when you want to delegate to another agent (e.g. a Codex",
|
|
21
|
+
"reviewer, a second Claude, or a Gemma chat).",
|
|
22
|
+
"",
|
|
23
|
+
"Runtimes: codex (background, via vctl) · gemma (background) · claude (terminal).",
|
|
24
|
+
"(--runtime defaults to codex. Gemma is conversational/read-only only — it",
|
|
25
|
+
" rejects --permission read-write.)",
|
|
26
|
+
"",
|
|
27
|
+
"Spawn one agent (CLI, non-interactive):",
|
|
28
|
+
" vibe-master spawn --permission <read-only|read-write> \\",
|
|
29
|
+
" [--runtime codex|claude|gemma] [--name <id>] [--scope <name>]",
|
|
30
|
+
" → only --permission is required; --runtime and --name are optional",
|
|
31
|
+
" (--name auto-generated if omitted). Prints the agent id; it registers",
|
|
32
|
+
" in Viber and comes online.",
|
|
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
|
+
"",
|
|
39
|
+
"Interactive: `vibe-master tui` (menu: + Add agent → runtime → permission → name).",
|
|
40
|
+
"Inspect / stop: `vibe-master list` · `vibe-master kill <id>`.",
|
|
41
|
+
"",
|
|
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.",
|
|
48
|
+
"Requires vibe-master installed on the machine (see the Install page on the web).",
|
|
49
|
+
"Run `vibe-master --help` for the full command reference.",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
package/lib/lockfile.ts
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* axis that isolates them for a normal bunx client, where sessionId is always
|
|
15
15
|
* "" (the #259 sessionId axis below provides no isolation there).
|
|
16
16
|
*
|
|
17
|
-
* #259 adds the per-instance axis: when
|
|
18
|
-
*
|
|
17
|
+
* #259 adds the per-instance axis: when a launcher supplies a per-launch session
|
|
18
|
+
* id via VIBER_CHANNEL_SESSION_ID, the lock is
|
|
19
19
|
* additionally namespaced by it, so two concurrent launches of the *same*
|
|
20
20
|
* project each get their own lock. Channel respawns within one launch inherit
|
|
21
21
|
* the same env var and reuse the same lock — accidental duplicates are still
|
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
|
-
*
|
|
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
|
|
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
|
|
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 {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* viber-channel stale-version guard (#330).
|
|
3
|
+
*
|
|
4
|
+
* A globally-installed `viber-channel` that a user placed by hand (old docs) can
|
|
5
|
+
* fall far behind npm `latest`. When a later refactor removes/renames a lib file,
|
|
6
|
+
* that stale install crashes with a raw `Cannot find module './lib/…'` instead of
|
|
7
|
+
* anything actionable. We can't fix an ALREADY-broken old install from here (its
|
|
8
|
+
* code predates this guard), so the useful move is PREVENTION: on every startup
|
|
9
|
+
* that still runs, check whether the installed version is behind `latest` and, if
|
|
10
|
+
* so, print a clear one-line nudge to update — BEFORE the user ever hits a
|
|
11
|
+
* breaking refactor. Best-effort and non-fatal: any error (offline, timeout, odd
|
|
12
|
+
* registry response) is swallowed so the channel starts normally.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
const NPM_LATEST_URL = "https://registry.npmjs.org/viber-channel/latest";
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
20
|
+
|
|
21
|
+
/** Parse a `major.minor.patch` string to a numeric tuple; non-numeric → 0. */
|
|
22
|
+
function parseVersion(v: string): [number, number, number] {
|
|
23
|
+
const parts = v
|
|
24
|
+
.trim()
|
|
25
|
+
.replace(/^v/, "")
|
|
26
|
+
.split(".")
|
|
27
|
+
.map((p) => Number.parseInt(p, 10));
|
|
28
|
+
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** True iff `local` is strictly older than `latest` (major.minor.patch order). */
|
|
32
|
+
export function isOlder(local: string, latest: string): boolean {
|
|
33
|
+
const a = parseVersion(local);
|
|
34
|
+
const b = parseVersion(latest);
|
|
35
|
+
for (let i = 0; i < 3; i++) {
|
|
36
|
+
if (a[i] < b[i]) return true;
|
|
37
|
+
if (a[i] > b[i]) return false;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Read this package's own version from its package.json (best-effort → null). */
|
|
43
|
+
export function readLocalVersion(
|
|
44
|
+
readFile: (p: string) => string = (p) => readFileSync(p, "utf-8"),
|
|
45
|
+
dir: string = import.meta.dir,
|
|
46
|
+
): string | null {
|
|
47
|
+
try {
|
|
48
|
+
const pkg = JSON.parse(readFile(join(dir, "..", "package.json")));
|
|
49
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Fetch npm `latest` version with a short timeout; null on any failure. */
|
|
56
|
+
export async function fetchLatestVersion(
|
|
57
|
+
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
|
58
|
+
fetchImpl: typeof fetch = fetch,
|
|
59
|
+
): Promise<string | null> {
|
|
60
|
+
const ctrl = new AbortController();
|
|
61
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetchImpl(NPM_LATEST_URL, { signal: ctrl.signal });
|
|
64
|
+
if (!res.ok) return null;
|
|
65
|
+
const body = (await res.json()) as { version?: unknown };
|
|
66
|
+
return typeof body.version === "string" ? body.version : null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
} finally {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface StaleCheck {
|
|
75
|
+
local: string;
|
|
76
|
+
latest: string;
|
|
77
|
+
stale: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Compare the installed version against npm latest. Null when either is unknown. */
|
|
81
|
+
export async function checkStaleVersion(opts?: {
|
|
82
|
+
localVersion?: string | null;
|
|
83
|
+
fetchLatest?: () => Promise<string | null>;
|
|
84
|
+
}): Promise<StaleCheck | null> {
|
|
85
|
+
// Honor an EXPLICIT localVersion (incl. null = "unknown"); only read the real
|
|
86
|
+
// package.json when the caller didn't pass the key at all.
|
|
87
|
+
const local =
|
|
88
|
+
opts && "localVersion" in opts ? opts.localVersion : readLocalVersion();
|
|
89
|
+
if (!local) return null;
|
|
90
|
+
const latest = opts?.fetchLatest
|
|
91
|
+
? await opts.fetchLatest()
|
|
92
|
+
: await fetchLatestVersion();
|
|
93
|
+
if (!latest) return null;
|
|
94
|
+
return { local, latest, stale: isOlder(local, latest) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The one-line nudge printed to stderr when the install is behind. */
|
|
98
|
+
export function staleWarning(local: string, latest: string): string {
|
|
99
|
+
return (
|
|
100
|
+
`[viber-channel] version ${local} is behind latest ${latest}. ` +
|
|
101
|
+
`Update to avoid a broken install: bun add -g viber-channel@latest`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Best-effort: warn on stderr when this install is behind npm latest. Never
|
|
107
|
+
* throws, never blocks longer than the fetch timeout. Called at startup.
|
|
108
|
+
*/
|
|
109
|
+
export async function warnIfStale(opts?: {
|
|
110
|
+
localVersion?: string | null;
|
|
111
|
+
fetchLatest?: () => Promise<string | null>;
|
|
112
|
+
write?: (msg: string) => void;
|
|
113
|
+
}): Promise<void> {
|
|
114
|
+
try {
|
|
115
|
+
const check = await checkStaleVersion(opts);
|
|
116
|
+
if (check?.stale) {
|
|
117
|
+
(opts?.write ?? ((m) => process.stderr.write(`${m}\n`)))(
|
|
118
|
+
staleWarning(check.local, check.latest),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
/* best-effort — never break startup on a version check */
|
|
123
|
+
}
|
|
124
|
+
}
|
package/package.json
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "viber-channel",
|
|
3
|
-
"version": "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
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
*
|
|
17
17
|
* claude --dangerously-load-development-channels server:<name>
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* For dev in this repo, vibe-master-dev.ps1 (the multi-agent TUI launcher) wraps
|
|
20
|
+
* the flag: `server:viber-dev-channel` against the dev backend. Staging sessions
|
|
21
|
+
* use the plain user-scoped `viber-channel` registration with no launcher flag.
|
|
22
22
|
*
|
|
23
23
|
* CLI subcommand: `bunx viber-channel connect <claim_url>` runs the one-shot
|
|
24
24
|
* claim flow that writes .viber/auth.json without requiring any Claude Code
|
|
@@ -54,6 +54,9 @@ import {
|
|
|
54
54
|
type TokenRefreshScheduler,
|
|
55
55
|
} from "./lib/token_refresh.ts";
|
|
56
56
|
import { awaitStableStartup } from "./lib/startup_gate.ts";
|
|
57
|
+
import { agentIdentityFromEnv } from "./lib/bridge_core.ts";
|
|
58
|
+
import { warnIfStale } from "./lib/version_check.ts";
|
|
59
|
+
import { capabilitiesText } from "./lib/capabilities.ts";
|
|
57
60
|
|
|
58
61
|
// ---- CLI subcommand dispatch (must happen before lock acquire + loadAuth) ----
|
|
59
62
|
//
|
|
@@ -98,6 +101,11 @@ import { awaitStableStartup } from "./lib/startup_gate.ts";
|
|
|
98
101
|
);
|
|
99
102
|
process.exit(1);
|
|
100
103
|
}
|
|
104
|
+
// #330: this is the exact path that crashed on a stale hand-placed install
|
|
105
|
+
// (`bunx viber-channel connect` → missing ./lib/peers.ts). Await the check
|
|
106
|
+
// here so the "you're behind, update" nudge shows BEFORE the connect work
|
|
107
|
+
// (best-effort — never throws, short timeout).
|
|
108
|
+
await warnIfStale();
|
|
101
109
|
try {
|
|
102
110
|
await runConnect(claimUrl);
|
|
103
111
|
process.exit(0);
|
|
@@ -108,6 +116,11 @@ import { awaitStableStartup } from "./lib/startup_gate.ts";
|
|
|
108
116
|
}
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
// Server path (fell through the dispatch block): warn if this install is behind
|
|
120
|
+
// npm latest, but fire-and-forget so it never delays mcp.connect — the nudge
|
|
121
|
+
// lands on stderr shortly after startup (#330).
|
|
122
|
+
void warnIfStale();
|
|
123
|
+
|
|
111
124
|
// Lock file path: %APPDATA%/viber/ (Windows) or ~/.config/viber/ (Linux/Mac).
|
|
112
125
|
// Namespaced by (VIBER_BASE_URL, client_fingerprint, VIBER_CHANNEL_SESSION_ID) so:
|
|
113
126
|
// - channels at different backends (staging + dev) coexist (different base_url);
|
|
@@ -340,18 +353,32 @@ const mcp = new Server(
|
|
|
340
353
|
experimental: { "claude/channel": {} },
|
|
341
354
|
tools: {},
|
|
342
355
|
},
|
|
356
|
+
// #369 truncation fix: Claude Code truncates long MCP `instructions`
|
|
357
|
+
// ("…[truncated]") and this blob was ~3 KB → the TAIL was dropped before the
|
|
358
|
+
// agent saw it (that hid the agent's name #328, and threatened the #332
|
|
359
|
+
// pointer + exit-intent). So: CRITICAL lines are FRONT-LOADED (identity →
|
|
360
|
+
// core reply → actions/questions → anti-deadlock → capabilities → exit) and
|
|
361
|
+
// the nice-to-have style notes live at the tail where a cut is harmless.
|
|
362
|
+
// Condensed vs the old prose (same substance — reviewed). Identity is "" for
|
|
363
|
+
// JP's own session (no explicit-name flag) → the identity LINE is filtered
|
|
364
|
+
// out for him; the rest of the (now shorter, reordered) blob applies to every
|
|
365
|
+
// session including his.
|
|
343
366
|
instructions: [
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
"ACTIONS and QUESTIONS
|
|
348
|
-
|
|
349
|
-
"
|
|
350
|
-
|
|
351
|
-
"
|
|
352
|
-
"Rule of thumb: short and conversational, but readable — break points into a list rather than a wall of text; reserve the artifact for what is genuinely long or heavy.",
|
|
367
|
+
agentIdentityFromEnv(),
|
|
368
|
+
// --- critical: front-loaded so truncation can never drop them ---
|
|
369
|
+
'Voice transcripts arrive as <channel source="viber-channel"> events (the user\'s microphone speech). Reply with send_message. The `text` is BOTH read aloud (TTS) AND shown as Markdown — make it natural aloud AND easy to read; lead with the answer, no preamble.',
|
|
370
|
+
"Put ACTIONS and QUESTIONS in `text`, visibly — never bury them in the artifact. Use light Markdown (short bullet/numbered lists, **bold**) to stay readable; a short spoken list is fine.",
|
|
371
|
+
// #343 anti-deadlock (hands-free: the user is NOT watching the terminal).
|
|
372
|
+
"CRITICAL — while this channel is active, NEVER block on a terminal prompt or AskUserQuestion: the user is hands-free and cannot see the terminal, so it deadlocks. Put EVERY question or choice in send_message `text` and take the answer from the next voice transcript.",
|
|
373
|
+
// #332 discoverability pointer (the how-to lives in the capabilities tool).
|
|
374
|
+
"To orchestrate or spawn OTHER agents (Codex, Claude, Gemma) on this machine, call the `capabilities` tool for how — only when relevant.",
|
|
353
375
|
"On exit intent (bye, au revoir, stop) call stop_conversation(), speak a brief farewell, and stop.",
|
|
354
|
-
|
|
376
|
+
// --- nice-to-have style notes (safe near the tail) ---
|
|
377
|
+
"The `artifact` (format: markdown default, or code/json/html) is for HEAVY/LONG content (big code, large tables, long analyses, JSON, file lists); keep `text` a brief spoken summary that points to it ('details on the side'). Scripts/event handlers are stripped server-side.",
|
|
378
|
+
"NO EMOJI in `text` (read aloud — an emoji becomes spoken noise). Write identifiers LITERALLY (288, auth.json, viber-dev.dgypx.dev) — never spell out dots/dashes; a lone long token or path is better placed in the artifact.",
|
|
379
|
+
]
|
|
380
|
+
.filter((line) => line.length > 0)
|
|
381
|
+
.join(" "),
|
|
355
382
|
}
|
|
356
383
|
);
|
|
357
384
|
|
|
@@ -401,10 +428,12 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
401
428
|
{
|
|
402
429
|
name: "list_agents",
|
|
403
430
|
description:
|
|
404
|
-
"List the OTHER agents (instances)
|
|
431
|
+
"List the OTHER agents (instances) you can talk to directly (#288). " +
|
|
405
432
|
"Returns each agent's id, label, runtime kind, and whether it is currently online. " +
|
|
406
433
|
"Use this to find the id of an agent (e.g. 'Codex Review') before calling message_agent. " +
|
|
407
|
-
"
|
|
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.",
|
|
408
437
|
inputSchema: {
|
|
409
438
|
type: "object" as const,
|
|
410
439
|
properties: {},
|
|
@@ -415,10 +444,12 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
415
444
|
{
|
|
416
445
|
name: "message_agent",
|
|
417
446
|
description:
|
|
418
|
-
"Send a direct message to another agent
|
|
447
|
+
"Send a direct message to another agent (#288). " +
|
|
419
448
|
"Pass the target agent's instance id (from list_agents, or the from_instance_id of a DM you received) " +
|
|
420
449
|
"and the message text. Opens or reuses a private 1:1 DM with that agent and posts your message; " +
|
|
421
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). " +
|
|
422
453
|
"Fails if the target agent is offline.",
|
|
423
454
|
inputSchema: {
|
|
424
455
|
type: "object" as const,
|
|
@@ -438,6 +469,20 @@ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
438
469
|
additionalProperties: false,
|
|
439
470
|
},
|
|
440
471
|
},
|
|
472
|
+
{
|
|
473
|
+
name: "capabilities",
|
|
474
|
+
description:
|
|
475
|
+
"On-demand: learn what the Viber control plane (vibe-master) can do on this " +
|
|
476
|
+
"machine — how to spawn and supervise OTHER agents (Codex, Claude, Gemma). " +
|
|
477
|
+
"Call this only when orchestrating other agents is relevant; it is not part " +
|
|
478
|
+
"of the default context. Returns a short usage reference.",
|
|
479
|
+
inputSchema: {
|
|
480
|
+
type: "object" as const,
|
|
481
|
+
properties: {},
|
|
482
|
+
required: [],
|
|
483
|
+
additionalProperties: false,
|
|
484
|
+
},
|
|
485
|
+
},
|
|
441
486
|
],
|
|
442
487
|
}));
|
|
443
488
|
|
|
@@ -478,6 +523,11 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
478
523
|
return libMessageAgent(channelToolsCtx, args);
|
|
479
524
|
}
|
|
480
525
|
|
|
526
|
+
// #332: on-demand control-plane discovery — static help, no runtime state.
|
|
527
|
+
if (request.params.name === "capabilities") {
|
|
528
|
+
return { content: [{ type: "text" as const, text: capabilitiesText() }] };
|
|
529
|
+
}
|
|
530
|
+
|
|
481
531
|
if (request.params.name !== "send_message") {
|
|
482
532
|
return {
|
|
483
533
|
isError: true,
|
|
@@ -829,7 +879,7 @@ try {
|
|
|
829
879
|
// ---- Channel ready banner ----
|
|
830
880
|
|
|
831
881
|
process.stderr.write(
|
|
832
|
-
`[viber-channel] Channel ready:
|
|
882
|
+
`[viber-channel] Channel ready: 4 tools (send_message, list_agents, message_agent, capabilities), SSE on /api/conversations/${CONVERSATION_ID}/events\n`
|
|
833
883
|
);
|
|
834
884
|
|
|
835
885
|
// Channel liveness is proven by the INSTANCE heartbeat (#311, started above), not
|