viber-channel 0.8.11 → 0.8.12
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/runner_exec.ts +80 -13
- package/lib/spawn_reason.ts +214 -0
- package/package.json +1 -1
package/lib/runner_exec.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { randomBytes } from "node:crypto";
|
|
|
18
18
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
20
|
import { cfAccessHeaders } from "./cfAccess.js";
|
|
21
|
+
import { messageForReason, parseSpawnResult, rosterNames, type SpawnReason } from "./spawn_reason.js";
|
|
21
22
|
|
|
22
23
|
/** A validated role from the server snapshot (allowlisted shape). */
|
|
23
24
|
export interface SnapshotRole {
|
|
@@ -148,7 +149,13 @@ export function validateAgainstPolicy(cmd: ClaimedCommand, policy: RunnerPolicy)
|
|
|
148
149
|
|
|
149
150
|
export type TeamSpawnRunner = (
|
|
150
151
|
args: string[],
|
|
151
|
-
|
|
152
|
+
/** #496: the nonce the child must echo on its outcome marker for it to be
|
|
153
|
+
* believed. REQUIRED (Codex review): an optional nonce lets a future launcher
|
|
154
|
+
* omit it and silently fall back to trusting any marker — the very hole the
|
|
155
|
+
* explicit trust parameter closes. A mock may ignore the argument; it cannot
|
|
156
|
+
* forget that it exists. */
|
|
157
|
+
nonce: string,
|
|
158
|
+
) => Promise<{ ok: boolean; recap: string; reason?: SpawnReason; detail?: string }>;
|
|
152
159
|
|
|
153
160
|
/**
|
|
154
161
|
* Build a CURATED child environment (Codex review, blocking): `execFile` would
|
|
@@ -202,7 +209,7 @@ export function vibeMasterBaseCmd(source: NodeJS.ProcessEnv = process.env): stri
|
|
|
202
209
|
/** Default launcher: shell vibe-master via execFile with an ARGV ARRAY (never a
|
|
203
210
|
* shell string), a CURATED env, bounded time + output, and process-tree kill on
|
|
204
211
|
* timeout. The binary is resolved via VIBE_MASTER_CMD (default `vibe-master`). */
|
|
205
|
-
export const execFileTeamSpawn: TeamSpawnRunner = (args) => {
|
|
212
|
+
export const execFileTeamSpawn: TeamSpawnRunner = (args, nonce) => {
|
|
206
213
|
const base = vibeMasterBaseCmd();
|
|
207
214
|
const bin = base[0] as string;
|
|
208
215
|
return new Promise((resolve) => {
|
|
@@ -211,8 +218,19 @@ export const execFileTeamSpawn: TeamSpawnRunner = (args) => {
|
|
|
211
218
|
[...base.slice(1), ...args],
|
|
212
219
|
{ timeout: 5 * 60 * 1000, killSignal: "SIGKILL", maxBuffer: 1024 * 1024, env: buildChildEnv() },
|
|
213
220
|
(err, stdout, stderr) => {
|
|
221
|
+
// #496: read the outcome marker from RAW stderr, BEFORE redaction and
|
|
222
|
+
// before the 4 000-char cut. The marker is written last, so it is the
|
|
223
|
+
// first thing `.slice()` drops on a real (verbose) roster — parsing the
|
|
224
|
+
// recap would pass every test and fail every actual spawn. Redaction can
|
|
225
|
+
// also mangle the JSON. Both hazards disappear by reading first.
|
|
226
|
+
const parsed = parseSpawnResult(stderr ?? "", { nonce });
|
|
214
227
|
const recap = redactSecrets(`${stdout}\n${stderr}`.trim()).slice(0, 4000);
|
|
215
|
-
resolve({
|
|
228
|
+
resolve({
|
|
229
|
+
ok: !err,
|
|
230
|
+
recap,
|
|
231
|
+
...(parsed?.reason ? { reason: parsed.reason } : {}),
|
|
232
|
+
...(parsed?.detail ? { detail: parsed.detail } : {}),
|
|
233
|
+
});
|
|
216
234
|
},
|
|
217
235
|
);
|
|
218
236
|
});
|
|
@@ -290,6 +308,18 @@ export function writeRunnerLog(commandId: string, recap: string, cwd: string = p
|
|
|
290
308
|
return path;
|
|
291
309
|
}
|
|
292
310
|
|
|
311
|
+
/** A `failed` report body carrying a structured reason (#496). The sentence is
|
|
312
|
+
* derived from the REASON, so what the web shows is server/runner-authored copy,
|
|
313
|
+
* never a raw client string; `error` keeps the operator-facing precision the
|
|
314
|
+
* local log used to hold alone. */
|
|
315
|
+
function refusalBody(
|
|
316
|
+
cmd: ClaimedCommand,
|
|
317
|
+
reason: SpawnReason,
|
|
318
|
+
error: string,
|
|
319
|
+
): Record<string, unknown> {
|
|
320
|
+
return { fencing_token: cmd.fencing_token, status: "failed", reason, error };
|
|
321
|
+
}
|
|
322
|
+
|
|
293
323
|
async function report(
|
|
294
324
|
auth: RunnerAuthLite,
|
|
295
325
|
cmdId: string,
|
|
@@ -331,14 +361,24 @@ export async function executeClaim(
|
|
|
331
361
|
try {
|
|
332
362
|
policy = deps.policy ?? loadRunnerPolicy();
|
|
333
363
|
} catch (err) {
|
|
334
|
-
const
|
|
335
|
-
await report(
|
|
364
|
+
const cause = err instanceof RunnerPolicyInvalidError ? err.message : "invalid local policy";
|
|
365
|
+
await report(
|
|
366
|
+
auth,
|
|
367
|
+
cmd.id,
|
|
368
|
+
refusalBody(cmd, "policy_invalid", `local policy invalid: ${cause}`),
|
|
369
|
+
fetchImpl,
|
|
370
|
+
);
|
|
336
371
|
return "failed";
|
|
337
372
|
}
|
|
338
373
|
|
|
339
374
|
const refusal = validateAgainstPolicy(cmd, policy);
|
|
340
375
|
if (refusal) {
|
|
341
|
-
await report(
|
|
376
|
+
await report(
|
|
377
|
+
auth,
|
|
378
|
+
cmd.id,
|
|
379
|
+
refusalBody(cmd, "policy_refused", `local policy: ${refusal}`),
|
|
380
|
+
fetchImpl,
|
|
381
|
+
);
|
|
342
382
|
return "failed";
|
|
343
383
|
}
|
|
344
384
|
|
|
@@ -350,7 +390,7 @@ export async function executeClaim(
|
|
|
350
390
|
const s = await report(
|
|
351
391
|
auth,
|
|
352
392
|
cmd.id,
|
|
353
|
-
|
|
393
|
+
refusalBody(cmd, "partial_run", "prior partial run detected on this runner — not relaunched"),
|
|
354
394
|
fetchImpl,
|
|
355
395
|
);
|
|
356
396
|
if (s >= 200 && s < 300) inflight.clear(cmd.id);
|
|
@@ -360,7 +400,12 @@ export async function executeClaim(
|
|
|
360
400
|
// template_name is the positional <name> in argv: positive allowlist (no
|
|
361
401
|
// leading '-', no control char) — argv-flag-injection defense.
|
|
362
402
|
if (!/^[A-Za-z0-9_.][A-Za-z0-9 _.-]{0,63}$/.test(cmd.template_name)) {
|
|
363
|
-
await report(
|
|
403
|
+
await report(
|
|
404
|
+
auth,
|
|
405
|
+
cmd.id,
|
|
406
|
+
refusalBody(cmd, "unsafe_template_name", "unsafe template_name"),
|
|
407
|
+
fetchImpl,
|
|
408
|
+
);
|
|
364
409
|
return "failed";
|
|
365
410
|
}
|
|
366
411
|
|
|
@@ -378,21 +423,43 @@ export async function executeClaim(
|
|
|
378
423
|
const { path: specPath, cleanup } = (deps.writeSpec ?? fsWriteSpec())(spec);
|
|
379
424
|
try {
|
|
380
425
|
// Allowlisted argv (C3): fixed flags + validated values, no free string.
|
|
381
|
-
|
|
426
|
+
// #496: a fresh nonce per launch, echoed back on the outcome marker.
|
|
427
|
+
// Threat model, stated precisely (Codex review): this DE-CORRELATES an
|
|
428
|
+
// accidental look-alike — an agent that prints this contract in its own
|
|
429
|
+
// output on the shared, deliberately-inherited stderr — which is the case
|
|
430
|
+
// that actually happens here. It is NOT authentication: a same-user process
|
|
431
|
+
// can read the parent's command line on Windows and forge the value. Nothing
|
|
432
|
+
// on this path can defend against a hostile local process, which already runs
|
|
433
|
+
// as the user; what protects the RENDERED text is the roster intersection
|
|
434
|
+
// below, not this nonce.
|
|
435
|
+
const nonce = randomBytes(16).toString("hex");
|
|
436
|
+
const args = ["team", "spawn", "--spec-file", specPath, "--prefix", cmd.prefix, "--result-nonce", nonce];
|
|
382
437
|
if (cmd.team_name) args.push("--team", cmd.team_name);
|
|
383
438
|
if (cmd.env) args.push("--env", cmd.env);
|
|
384
439
|
|
|
385
440
|
inflight.mark(cmd.id); // durable "started here" BEFORE launch (C4)
|
|
386
|
-
const { ok, recap } = await runTeamSpawn(args);
|
|
387
|
-
|
|
388
|
-
//
|
|
441
|
+
const { ok, recap, reason, detail } = await runTeamSpawn(args, nonce);
|
|
442
|
+
|
|
443
|
+
// #496: the failure now travels as a REASON plus a sentence built from it —
|
|
444
|
+
// whoever clicked Spawn in a browser can act on what they read. The full
|
|
445
|
+
// recap still goes ONLY to the 0600 local log (no secret leak); what changed
|
|
446
|
+
// is that the local log is no longer the sole place the cause exists. With no
|
|
447
|
+
// marker (older vibe-master, output lost to maxBuffer) we degrade to exactly
|
|
448
|
+
// the previous message rather than inventing a cause.
|
|
389
449
|
const logPath = deps.writeLog ? deps.writeLog(cmd.id, recap) : writeRunnerLog(cmd.id, recap);
|
|
390
450
|
const finalStatus = await report(
|
|
391
451
|
auth,
|
|
392
452
|
cmd.id,
|
|
393
453
|
ok
|
|
394
454
|
? { fencing_token: cmd.fencing_token, status: "succeeded", result: { message: "team spawn completed", log: logPath } }
|
|
395
|
-
: {
|
|
455
|
+
: {
|
|
456
|
+
fencing_token: cmd.fencing_token,
|
|
457
|
+
status: "failed",
|
|
458
|
+
error: reason
|
|
459
|
+
? messageForReason(reason, detail, rosterNames(cmd.prefix, cmd.template_spec.roles))
|
|
460
|
+
: "team spawn failed — see local runner log",
|
|
461
|
+
...(reason ? { reason } : {}),
|
|
462
|
+
},
|
|
396
463
|
fetchImpl,
|
|
397
464
|
);
|
|
398
465
|
// Clear the inflight marker ONLY when the TERMINAL report is accepted (2xx)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spawn_reason.ts — the runner's half of the spawn-outcome contract (#496).
|
|
3
|
+
*
|
|
4
|
+
* A spawn refused on this machine used to reach the web as one sentence,
|
|
5
|
+
* "team spawn failed — see local runner log". Whoever clicked Spawn from a
|
|
6
|
+
* browser has no terminal and no access to that log: they saw a red failure and
|
|
7
|
+
* had nowhere to go. The cause existed — it was on the runner's stderr — and was
|
|
8
|
+
* thrown away here.
|
|
9
|
+
*
|
|
10
|
+
* `vibe-master` now emits a machine line (`VIBEMASTER_RESULT {"reason":…}`,
|
|
11
|
+
* see vibe-master/lib/spawn_result.ts). This module extracts it and turns a
|
|
12
|
+
* reason into a sentence the web can show. The runner's OWN refusals (policy,
|
|
13
|
+
* unsafe input, a prior partial run) carry reasons from the same vocabulary, so
|
|
14
|
+
* the web never has to speak two languages.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Reasons a spawn can be refused. Mirrors vibe-master's vocabulary plus the
|
|
18
|
+
* four the runner produces itself. The API keeps its own allowlist: an unknown
|
|
19
|
+
* reason is dropped there, never stored raw — it comes from a client machine. */
|
|
20
|
+
export const SPAWN_REASONS = [
|
|
21
|
+
// Emitted by vibe-master
|
|
22
|
+
"already_running",
|
|
23
|
+
"partial_team",
|
|
24
|
+
"template_invalid",
|
|
25
|
+
"codex_unavailable",
|
|
26
|
+
"launch_failed",
|
|
27
|
+
// Emitted by the runner itself
|
|
28
|
+
"policy_invalid",
|
|
29
|
+
"policy_refused",
|
|
30
|
+
"unsafe_template_name",
|
|
31
|
+
"partial_run",
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
export type SpawnReason = (typeof SPAWN_REASONS)[number];
|
|
35
|
+
|
|
36
|
+
const MARKER = "VIBEMASTER_RESULT";
|
|
37
|
+
|
|
38
|
+
/** Detail is diagnostic context, never shown raw to a user (the server bounds it
|
|
39
|
+
* again and the web renders from the REASON). Bounded here too so a runaway
|
|
40
|
+
* child cannot push a megabyte into a report. */
|
|
41
|
+
const DETAIL_MAX = 300;
|
|
42
|
+
|
|
43
|
+
export interface ParsedSpawnResult {
|
|
44
|
+
reason: SpawnReason;
|
|
45
|
+
detail?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Keep only the names that belong to THIS command's roster.
|
|
50
|
+
*
|
|
51
|
+
* A charset check is not enough (Codex review): `session-expired, sign-in, now`
|
|
52
|
+
* satisfies any agent-id pattern and would be rendered as though it listed
|
|
53
|
+
* members. The only trustworthy authority on what a name may be is the roster the
|
|
54
|
+
* SERVER validated and sent — so `detail` is not sanitized, it is INTERSECTED
|
|
55
|
+
* with that roster. A name the server never asked to spawn cannot reach the
|
|
56
|
+
* screen, whatever the runner sent, and prose cannot survive at all.
|
|
57
|
+
*/
|
|
58
|
+
const MAX_NAMES = 10;
|
|
59
|
+
|
|
60
|
+
export function keepRosterNames(
|
|
61
|
+
detail: string | undefined,
|
|
62
|
+
roster: readonly string[],
|
|
63
|
+
): string | undefined {
|
|
64
|
+
if (!detail) return undefined;
|
|
65
|
+
const allowed = new Set(roster);
|
|
66
|
+
const names = detail
|
|
67
|
+
.split(",")
|
|
68
|
+
.map((t) => t.trim())
|
|
69
|
+
.filter((t) => allowed.has(t))
|
|
70
|
+
.slice(0, MAX_NAMES);
|
|
71
|
+
return names.length > 0 ? names.join(", ") : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The agent names a command may talk about: `{prefix}-{role}`, plus
|
|
76
|
+
* `{prefix}-{role}-{n}` for replicated roles. Mirrors vibe-master's naming in
|
|
77
|
+
* `resolveTeamSpawn` — the two must move together.
|
|
78
|
+
*/
|
|
79
|
+
export function rosterNames(
|
|
80
|
+
prefix: string,
|
|
81
|
+
roles: readonly { role: string; count?: number }[],
|
|
82
|
+
): string[] {
|
|
83
|
+
const names: string[] = [];
|
|
84
|
+
for (const r of roles) {
|
|
85
|
+
const count = r.count && r.count > 1 ? r.count : 1;
|
|
86
|
+
if (count === 1) {
|
|
87
|
+
names.push(`${prefix}-${r.role}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (let i = 1; i <= count; i++) names.push(`${prefix}-${r.role}-${i}`);
|
|
91
|
+
}
|
|
92
|
+
return names;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract the outcome marker from RAW child stderr.
|
|
97
|
+
*
|
|
98
|
+
* Read this before the recap is built, never after: the recap is redacted and
|
|
99
|
+
* then cut to 4 000 characters, and the marker is written LAST — exactly the end
|
|
100
|
+
* that the cut removes. A four-member roster overflows that budget easily, so
|
|
101
|
+
* parsing the recap would work in tests and fail on a real spawn.
|
|
102
|
+
*
|
|
103
|
+
* Scans for the LAST line that starts with the marker AND parses AND carries a
|
|
104
|
+
* known reason — not simply the last line. On `launch_failed` the marker is
|
|
105
|
+
* written after child processes have inherited this same stderr, so trailing
|
|
106
|
+
* child output can follow it; and a line quoting the marker inside other text is
|
|
107
|
+
* ignored because the prefix must start the line.
|
|
108
|
+
*
|
|
109
|
+
* Returns null when there is no marker (an older vibe-master, or output lost to
|
|
110
|
+
* a maxBuffer overflow) — the caller then behaves exactly as before.
|
|
111
|
+
*/
|
|
112
|
+
export type MarkerTrust =
|
|
113
|
+
/** Believe only a marker echoing this nonce (the runner path — the default). */
|
|
114
|
+
| { nonce: string }
|
|
115
|
+
/** No emitter check at all. Named so a caller cannot fall into it by omission
|
|
116
|
+
* (Opus review): an optional nonce silently skipped would reopen "last valid
|
|
117
|
+
* marker wins" with no test turning red. Only for reading output nobody
|
|
118
|
+
* else could have written into — a human running the CLI, or a unit test. */
|
|
119
|
+
| { trustUnauthenticated: true };
|
|
120
|
+
|
|
121
|
+
export function parseSpawnResult(
|
|
122
|
+
rawStderr: string,
|
|
123
|
+
trust: MarkerTrust,
|
|
124
|
+
): ParsedSpawnResult | null {
|
|
125
|
+
const expectedNonce = "nonce" in trust ? trust.nonce : undefined;
|
|
126
|
+
if (expectedNonce !== undefined && expectedNonce.length === 0) {
|
|
127
|
+
throw new Error("parseSpawnResult: empty nonce — pass {trustUnauthenticated:true} to skip the check deliberately");
|
|
128
|
+
}
|
|
129
|
+
const known = new Set<string>(SPAWN_REASONS);
|
|
130
|
+
for (const line of rawStderr.split(/\r?\n/).reverse()) {
|
|
131
|
+
if (!line.startsWith(`${MARKER} `)) continue;
|
|
132
|
+
let parsed: unknown;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(line.slice(MARKER.length + 1));
|
|
135
|
+
} catch {
|
|
136
|
+
continue; // truncated or interleaved — keep looking further back
|
|
137
|
+
}
|
|
138
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
139
|
+
const { reason, detail, nonce } = parsed as {
|
|
140
|
+
reason?: unknown;
|
|
141
|
+
detail?: unknown;
|
|
142
|
+
nonce?: unknown;
|
|
143
|
+
};
|
|
144
|
+
// Emitter proof. Spawned agents inherit this stderr, and an agent working on
|
|
145
|
+
// THIS repo prints this very contract in its own output — "the last valid
|
|
146
|
+
// marker wins" would let such a line overrule the real outcome and turn a
|
|
147
|
+
// launch_failed into a neutral already_running. Only the process we handed
|
|
148
|
+
// the nonce to can echo it.
|
|
149
|
+
if (expectedNonce && nonce !== expectedNonce) continue;
|
|
150
|
+
if (typeof reason !== "string" || !known.has(reason)) continue;
|
|
151
|
+
return {
|
|
152
|
+
reason: reason as SpawnReason,
|
|
153
|
+
...(typeof detail === "string" && detail.length > 0
|
|
154
|
+
? { detail: detail.slice(0, DETAIL_MAX) }
|
|
155
|
+
: {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The sentence the web shows. Built from the REASON. Where agent names carry the
|
|
163
|
+
* actionable part ("which member is missing"), they go through
|
|
164
|
+
* {@link keepRosterNames} first, so what is interpolated is provably a subset of
|
|
165
|
+
* the roster the server itself validated — never a sentence a machine chose.
|
|
166
|
+
* The roster is REQUIRED (Opus review): defaulting it to `[]` would let a caller
|
|
167
|
+
* drop every name in silence — the same "bypass by omission" the explicit marker
|
|
168
|
+
* trust closes, failing safe but failing quietly. A caller with no roster passes
|
|
169
|
+
* `[]` deliberately, and that shows in the diff.
|
|
170
|
+
*
|
|
171
|
+
* `policy_refused` interpolates NOTHING: its detail is free-form policy text, not
|
|
172
|
+
* a name list, so it cannot be shape-checked. The precision stays in the
|
|
173
|
+
* operator-facing `error`, off the screen.
|
|
174
|
+
*/
|
|
175
|
+
export function messageForReason(
|
|
176
|
+
reason: SpawnReason,
|
|
177
|
+
detail?: string,
|
|
178
|
+
roster: readonly string[],
|
|
179
|
+
): string {
|
|
180
|
+
const safe = keepRosterNames(detail, roster);
|
|
181
|
+
const names = safe ? ` (${safe})` : "";
|
|
182
|
+
switch (reason) {
|
|
183
|
+
case "already_running":
|
|
184
|
+
// ATTRIBUTED, not asserted (Opus review). Of the nine reasons this is the
|
|
185
|
+
// ONLY one that renders as reassuring — every other paints red. So it is
|
|
186
|
+
// the one a buggy or compromised runner could use to claim "nothing was
|
|
187
|
+
// launched" while agents are actually running: the exact false green this
|
|
188
|
+
// issue exists to remove, re-entering through the pipe we just built. The
|
|
189
|
+
// server cannot verify the claim, so the copy SOURCES it instead. A user
|
|
190
|
+
// whose screen disagrees with their machine then knows what to doubt.
|
|
191
|
+
return `the runner reports that a team with this prefix was already running on this machine${names} — so nothing was launched`;
|
|
192
|
+
case "partial_team":
|
|
193
|
+
return `the runner reports that a team with this prefix is only PARTLY running — these members are missing${names}. Stop the survivors or pick a fresh prefix`;
|
|
194
|
+
case "template_invalid":
|
|
195
|
+
// On the web path this is NOT a user mistake: the runner never passes a
|
|
196
|
+
// template NAME, it writes the server-validated snapshot to a file and
|
|
197
|
+
// passes --spec-file. So this means the server's own snapshot could not be
|
|
198
|
+
// read back — a defect. Telling the user to go read a log they cannot open,
|
|
199
|
+
// for a problem they did not cause, would repeat this issue in miniature.
|
|
200
|
+
return "the roster sent for this spawn could not be read back on the runner machine — this is likely a defect, not something you did wrong; the full error is in the machine's log";
|
|
201
|
+
case "codex_unavailable":
|
|
202
|
+
return "codex is not launchable on this machine, so no agent was started — install codex or set VIBER_CODEX_BIN";
|
|
203
|
+
case "launch_failed":
|
|
204
|
+
return `a member failed to start${names}; the rest of the roster was skipped — the team is incomplete`;
|
|
205
|
+
case "policy_invalid":
|
|
206
|
+
return "this machine's local runner policy is present but invalid, so the spawn was refused";
|
|
207
|
+
case "policy_refused":
|
|
208
|
+
return "this machine's local policy refuses this spawn";
|
|
209
|
+
case "unsafe_template_name":
|
|
210
|
+
return "the template name was rejected as unsafe by the runner";
|
|
211
|
+
case "partial_run":
|
|
212
|
+
return "a prior run of this command already started here and was not relaunched — check the machine before retrying";
|
|
213
|
+
}
|
|
214
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viber-channel",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.12",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"bin": {
|