switchroom 0.15.25 → 0.15.27
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/dist/cli/switchroom.js +1293 -414
- package/dist/cli/ui/index.html +682 -60
- package/dist/host-control/main.js +279 -7
- package/package.json +3 -2
- package/telegram-plugin/dist/gateway/gateway.js +179 -18
- package/telegram-plugin/gateway/linear-activity.ts +102 -14
- package/telegram-plugin/tests/linear-agent-activity.test.ts +75 -0
- package/telegram-plugin/tests/linear-create-issue.test.ts +42 -0
|
@@ -15013,6 +15013,13 @@ function validateAllCronTopicAliases(config, filePath) {
|
|
|
15013
15013
|
throw new ConfigError(`Cron \`topic:\` alias references unknown topic_aliases in ${filePath}`, issues);
|
|
15014
15014
|
}
|
|
15015
15015
|
}
|
|
15016
|
+
function resolveAgentsDir(config) {
|
|
15017
|
+
const override = process.env.SWITCHROOM_AGENTS_DIR;
|
|
15018
|
+
if (override && override.length > 0 && override.startsWith("/")) {
|
|
15019
|
+
return override;
|
|
15020
|
+
}
|
|
15021
|
+
return resolveDualPath(config.switchroom.agents_dir);
|
|
15022
|
+
}
|
|
15016
15023
|
|
|
15017
15024
|
// src/agents/compose.ts
|
|
15018
15025
|
import { createHash } from "node:crypto";
|
|
@@ -15045,7 +15052,7 @@ var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
|
15045
15052
|
|
|
15046
15053
|
// src/host-control/server.ts
|
|
15047
15054
|
import { createServer } from "node:net";
|
|
15048
|
-
import { spawn, spawnSync as
|
|
15055
|
+
import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
15049
15056
|
import { mkdir, chmod, chown, unlink, appendFile } from "node:fs/promises";
|
|
15050
15057
|
import {
|
|
15051
15058
|
readdirSync as readdirSync2,
|
|
@@ -15148,6 +15155,20 @@ var DoctorRequestSchema = exports_external.object({
|
|
|
15148
15155
|
op: exports_external.literal("doctor"),
|
|
15149
15156
|
args: exports_external.object({}).optional()
|
|
15150
15157
|
});
|
|
15158
|
+
var AgentStatusRequestSchema = exports_external.object({
|
|
15159
|
+
...RequestEnvelope,
|
|
15160
|
+
op: exports_external.literal("agent_status"),
|
|
15161
|
+
args: exports_external.object({
|
|
15162
|
+
name: AgentNameSchema.optional()
|
|
15163
|
+
})
|
|
15164
|
+
});
|
|
15165
|
+
var AgentScheduleRequestSchema = exports_external.object({
|
|
15166
|
+
...RequestEnvelope,
|
|
15167
|
+
op: exports_external.literal("agent_schedule"),
|
|
15168
|
+
args: exports_external.object({
|
|
15169
|
+
name: AgentNameSchema.optional()
|
|
15170
|
+
})
|
|
15171
|
+
});
|
|
15151
15172
|
var AgentSmokeRequestSchema = exports_external.object({
|
|
15152
15173
|
...RequestEnvelope,
|
|
15153
15174
|
op: exports_external.literal("agent_smoke"),
|
|
@@ -15178,6 +15199,8 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
|
|
|
15178
15199
|
AgentExecRequestSchema,
|
|
15179
15200
|
DoctorRequestSchema,
|
|
15180
15201
|
AgentSmokeRequestSchema,
|
|
15202
|
+
AgentStatusRequestSchema,
|
|
15203
|
+
AgentScheduleRequestSchema,
|
|
15181
15204
|
ConfigProposeEditRequestSchema
|
|
15182
15205
|
]);
|
|
15183
15206
|
var ResultSchema = exports_external.enum(["started", "completed", "denied", "error"]);
|
|
@@ -15240,6 +15263,7 @@ var ResponseEnvelope = {
|
|
|
15240
15263
|
audit_id: exports_external.string().min(1).optional(),
|
|
15241
15264
|
stdout_tail: exports_external.string().optional(),
|
|
15242
15265
|
stderr_tail: exports_external.string().optional(),
|
|
15266
|
+
payload: exports_external.string().optional(),
|
|
15243
15267
|
error: exports_external.string().optional(),
|
|
15244
15268
|
error_envelope: ErrorEnvelopeSchema.optional()
|
|
15245
15269
|
};
|
|
@@ -20896,12 +20920,148 @@ function classifyBlastRadius(beforeYaml, afterYaml) {
|
|
|
20896
20920
|
};
|
|
20897
20921
|
}
|
|
20898
20922
|
|
|
20923
|
+
// src/agents/lifecycle.ts
|
|
20924
|
+
import { execFileSync, spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
20925
|
+
|
|
20926
|
+
// src/agents/tmux.ts
|
|
20927
|
+
var MAX_BYTES = 10 * 1024 * 1024;
|
|
20928
|
+
|
|
20929
|
+
// src/agents/lifecycle.ts
|
|
20930
|
+
function containerName(name) {
|
|
20931
|
+
return `switchroom-${name}`;
|
|
20932
|
+
}
|
|
20933
|
+
function getAllAgentStatuses(config) {
|
|
20934
|
+
const agentNames = Object.keys(config.agents);
|
|
20935
|
+
const statuses = {};
|
|
20936
|
+
if (agentNames.length === 0)
|
|
20937
|
+
return statuses;
|
|
20938
|
+
const cnByAgent = new Map;
|
|
20939
|
+
const agentByCn = new Map;
|
|
20940
|
+
for (const a of agentNames) {
|
|
20941
|
+
const cn = containerName(a);
|
|
20942
|
+
cnByAgent.set(a, cn);
|
|
20943
|
+
agentByCn.set(cn, a);
|
|
20944
|
+
statuses[a] = { active: "inactive", uptime: null, memory: null, pid: null };
|
|
20945
|
+
}
|
|
20946
|
+
const insp = spawnSync2("docker", [
|
|
20947
|
+
"inspect",
|
|
20948
|
+
"--format",
|
|
20949
|
+
"{{.Name}}|{{.State.Status}}|{{.State.StartedAt}}|{{.State.Pid}}",
|
|
20950
|
+
...cnByAgent.values()
|
|
20951
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15000 });
|
|
20952
|
+
for (const line of (insp.stdout ?? "").split(`
|
|
20953
|
+
`)) {
|
|
20954
|
+
const t = line.trim();
|
|
20955
|
+
if (!t)
|
|
20956
|
+
continue;
|
|
20957
|
+
const [rawName, status, startedAt, pidStr] = t.split("|");
|
|
20958
|
+
const cn = (rawName ?? "").replace(/^\//, "");
|
|
20959
|
+
const agent = agentByCn.get(cn);
|
|
20960
|
+
if (!agent)
|
|
20961
|
+
continue;
|
|
20962
|
+
const pidNum = parseInt(pidStr ?? "", 10);
|
|
20963
|
+
statuses[agent] = {
|
|
20964
|
+
active: status === "running" ? "active" : status || "inactive",
|
|
20965
|
+
uptime: startedAt && startedAt !== "0001-01-01T00:00:00Z" ? startedAt : null,
|
|
20966
|
+
memory: null,
|
|
20967
|
+
pid: Number.isFinite(pidNum) && pidNum > 0 ? pidNum : null
|
|
20968
|
+
};
|
|
20969
|
+
}
|
|
20970
|
+
const stats = spawnSync2("docker", ["stats", "--no-stream", "--format", "{{.Name}}|{{.MemUsage}}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15000 });
|
|
20971
|
+
if (stats.status === 0) {
|
|
20972
|
+
for (const line of (stats.stdout ?? "").split(`
|
|
20973
|
+
`)) {
|
|
20974
|
+
const t = line.trim();
|
|
20975
|
+
if (!t)
|
|
20976
|
+
continue;
|
|
20977
|
+
const [cn, memUsage] = t.split("|");
|
|
20978
|
+
const agent = cn ? agentByCn.get(cn) : undefined;
|
|
20979
|
+
if (!agent || statuses[agent].active !== "active")
|
|
20980
|
+
continue;
|
|
20981
|
+
const first = (memUsage ?? "").split("/")[0]?.trim();
|
|
20982
|
+
if (!first)
|
|
20983
|
+
continue;
|
|
20984
|
+
const m = first.match(/([\d.]+)\s*([KMG]i?B)/i);
|
|
20985
|
+
if (m) {
|
|
20986
|
+
const val = parseFloat(m[1]);
|
|
20987
|
+
const unit = m[2].toUpperCase();
|
|
20988
|
+
let mb = val;
|
|
20989
|
+
if (unit.startsWith("K"))
|
|
20990
|
+
mb = val / 1024;
|
|
20991
|
+
else if (unit.startsWith("G"))
|
|
20992
|
+
mb = val * 1024;
|
|
20993
|
+
statuses[agent].memory = `${Math.round(mb)}MB`;
|
|
20994
|
+
} else {
|
|
20995
|
+
statuses[agent].memory = first;
|
|
20996
|
+
}
|
|
20997
|
+
}
|
|
20998
|
+
}
|
|
20999
|
+
return statuses;
|
|
21000
|
+
}
|
|
21001
|
+
|
|
21002
|
+
// src/scheduler/dispatch.ts
|
|
21003
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
21004
|
+
function collectScheduleEntries(config) {
|
|
21005
|
+
const out = [];
|
|
21006
|
+
const agentNames = Object.keys(config.agents).sort();
|
|
21007
|
+
for (const agent of agentNames) {
|
|
21008
|
+
const raw = config.agents[agent];
|
|
21009
|
+
if (!raw)
|
|
21010
|
+
continue;
|
|
21011
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, raw);
|
|
21012
|
+
const schedule = resolved.schedule ?? [];
|
|
21013
|
+
for (let i = 0;i < schedule.length; i++) {
|
|
21014
|
+
const entry = schedule[i];
|
|
21015
|
+
const auditMaterial = entry.prompt ?? `action:${JSON.stringify(entry.action ?? {})}`;
|
|
21016
|
+
out.push({
|
|
21017
|
+
agent,
|
|
21018
|
+
scheduleIndex: i,
|
|
21019
|
+
cron: entry.cron,
|
|
21020
|
+
...entry.prompt !== undefined ? { prompt: entry.prompt } : {},
|
|
21021
|
+
promptKey: createHash4("sha256").update(auditMaterial).digest("hex").slice(0, 12),
|
|
21022
|
+
...entry.topic !== undefined ? { topic: entry.topic } : {},
|
|
21023
|
+
...entry.kind !== undefined ? { kind: entry.kind } : {},
|
|
21024
|
+
...entry.model !== undefined ? { model: entry.model } : {},
|
|
21025
|
+
...entry.context !== undefined ? { context: entry.context } : {},
|
|
21026
|
+
...entry.poll !== undefined ? { poll: entry.poll } : {},
|
|
21027
|
+
...entry.action !== undefined ? { action: entry.action } : {}
|
|
21028
|
+
});
|
|
21029
|
+
}
|
|
21030
|
+
}
|
|
21031
|
+
return out;
|
|
21032
|
+
}
|
|
21033
|
+
|
|
21034
|
+
// src/agent-scheduler/replay.ts
|
|
21035
|
+
var STALE_LOOKBACK_MAX_MIN = 14 * 24 * 60;
|
|
21036
|
+
function readRecentFires(jsonlPath) {
|
|
21037
|
+
const fs2 = __require("node:fs");
|
|
21038
|
+
if (!fs2.existsSync(jsonlPath))
|
|
21039
|
+
return [];
|
|
21040
|
+
let raw;
|
|
21041
|
+
try {
|
|
21042
|
+
raw = fs2.readFileSync(jsonlPath, "utf8");
|
|
21043
|
+
} catch {
|
|
21044
|
+
return [];
|
|
21045
|
+
}
|
|
21046
|
+
const out = [];
|
|
21047
|
+
for (const line of raw.split(`
|
|
21048
|
+
`)) {
|
|
21049
|
+
const trimmed = line.trim();
|
|
21050
|
+
if (trimmed.length === 0)
|
|
21051
|
+
continue;
|
|
21052
|
+
try {
|
|
21053
|
+
out.push(JSON.parse(trimmed));
|
|
21054
|
+
} catch {}
|
|
21055
|
+
}
|
|
21056
|
+
return out;
|
|
21057
|
+
}
|
|
21058
|
+
|
|
20899
21059
|
// src/host-control/server.ts
|
|
20900
21060
|
function resolveDigests(imageRefs) {
|
|
20901
21061
|
const out = new Map;
|
|
20902
21062
|
for (const ref of imageRefs) {
|
|
20903
21063
|
try {
|
|
20904
|
-
const r =
|
|
21064
|
+
const r = spawnSync3("docker", ["inspect", "--format={{index .RepoDigests 0}}", ref], { encoding: "utf-8", timeout: 5000 });
|
|
20905
21065
|
if (r.status !== 0)
|
|
20906
21066
|
continue;
|
|
20907
21067
|
const trimmed = (r.stdout ?? "").trim();
|
|
@@ -21204,6 +21364,12 @@ class HostdServer {
|
|
|
21204
21364
|
case "agent_smoke":
|
|
21205
21365
|
resp = await this.handleAgentSmoke(req, started);
|
|
21206
21366
|
break;
|
|
21367
|
+
case "agent_status":
|
|
21368
|
+
resp = this.handleAgentStatus(req, started);
|
|
21369
|
+
break;
|
|
21370
|
+
case "agent_schedule":
|
|
21371
|
+
resp = this.handleAgentSchedule(req, started);
|
|
21372
|
+
break;
|
|
21207
21373
|
case "config_propose_edit":
|
|
21208
21374
|
resp = await this.handleConfigProposeEdit(req, caller, started);
|
|
21209
21375
|
break;
|
|
@@ -21256,6 +21422,12 @@ class HostdServer {
|
|
|
21256
21422
|
return callerAdmin ? null : `${req.op} cross-agent requires admin: true on caller "${caller.name}"`;
|
|
21257
21423
|
case "doctor":
|
|
21258
21424
|
return callerAdmin ? null : `doctor requires admin: true on caller "${caller.name}"`;
|
|
21425
|
+
case "agent_status":
|
|
21426
|
+
case "agent_schedule":
|
|
21427
|
+
if (req.args.name !== undefined && req.args.name === caller.name) {
|
|
21428
|
+
return null;
|
|
21429
|
+
}
|
|
21430
|
+
return callerAdmin ? null : `${req.op} ${req.args.name === undefined ? "fleet view" : "cross-agent"} requires admin: true on caller "${caller.name}"`;
|
|
21259
21431
|
case "config_propose_edit":
|
|
21260
21432
|
return null;
|
|
21261
21433
|
}
|
|
@@ -21726,10 +21898,67 @@ class HostdServer {
|
|
|
21726
21898
|
}));
|
|
21727
21899
|
return respond("running", probes);
|
|
21728
21900
|
}
|
|
21901
|
+
handleAgentStatus(req, started) {
|
|
21902
|
+
try {
|
|
21903
|
+
const cfg = loadConfig(this.opts.configPath);
|
|
21904
|
+
const all = getAllAgentStatuses(cfg);
|
|
21905
|
+
const statuses = req.args.name ? req.args.name in all ? { [req.args.name]: all[req.args.name] } : {} : all;
|
|
21906
|
+
return {
|
|
21907
|
+
v: 1,
|
|
21908
|
+
request_id: req.request_id,
|
|
21909
|
+
result: "completed",
|
|
21910
|
+
exit_code: 0,
|
|
21911
|
+
duration_ms: Date.now() - started,
|
|
21912
|
+
payload: JSON.stringify({ statuses })
|
|
21913
|
+
};
|
|
21914
|
+
} catch (e) {
|
|
21915
|
+
return {
|
|
21916
|
+
v: 1,
|
|
21917
|
+
request_id: req.request_id,
|
|
21918
|
+
result: "error",
|
|
21919
|
+
exit_code: null,
|
|
21920
|
+
duration_ms: Date.now() - started,
|
|
21921
|
+
error: `agent_status failed: ${e.message}`
|
|
21922
|
+
};
|
|
21923
|
+
}
|
|
21924
|
+
}
|
|
21925
|
+
handleAgentSchedule(req, started) {
|
|
21926
|
+
try {
|
|
21927
|
+
const cfg = loadConfig(this.opts.configPath);
|
|
21928
|
+
let entries = collectScheduleEntries(cfg);
|
|
21929
|
+
if (req.args.name)
|
|
21930
|
+
entries = entries.filter((e) => e.agent === req.args.name);
|
|
21931
|
+
const agentsDir = resolveAgentsDir(cfg);
|
|
21932
|
+
const recentByAgent = {};
|
|
21933
|
+
for (const agent of new Set(entries.map((e) => e.agent))) {
|
|
21934
|
+
const rows = readRecentFires(resolve5(agentsDir, agent, "scheduler.jsonl"));
|
|
21935
|
+
if (rows.length > 0)
|
|
21936
|
+
recentByAgent[agent] = rows;
|
|
21937
|
+
}
|
|
21938
|
+
const bounded = boundScheduleView(entries, recentByAgent);
|
|
21939
|
+
return {
|
|
21940
|
+
v: 1,
|
|
21941
|
+
request_id: req.request_id,
|
|
21942
|
+
result: "completed",
|
|
21943
|
+
exit_code: 0,
|
|
21944
|
+
duration_ms: Date.now() - started,
|
|
21945
|
+
payload: JSON.stringify(bounded)
|
|
21946
|
+
};
|
|
21947
|
+
} catch (e) {
|
|
21948
|
+
return {
|
|
21949
|
+
v: 1,
|
|
21950
|
+
request_id: req.request_id,
|
|
21951
|
+
result: "error",
|
|
21952
|
+
exit_code: null,
|
|
21953
|
+
duration_ms: Date.now() - started,
|
|
21954
|
+
error: `agent_schedule failed: ${e.message}`
|
|
21955
|
+
};
|
|
21956
|
+
}
|
|
21957
|
+
}
|
|
21729
21958
|
runDocker(args) {
|
|
21730
21959
|
return new Promise((resolve6, reject) => {
|
|
21731
21960
|
const bin = this.opts.dockerBin ?? "docker";
|
|
21732
|
-
const child =
|
|
21961
|
+
const child = spawn2(bin, args, {
|
|
21733
21962
|
stdio: ["ignore", "pipe", "pipe"],
|
|
21734
21963
|
env: { ...process.env }
|
|
21735
21964
|
});
|
|
@@ -21752,7 +21981,7 @@ class HostdServer {
|
|
|
21752
21981
|
const composePath = join3(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
|
|
21753
21982
|
if (!existsSync7(composePath))
|
|
21754
21983
|
return [];
|
|
21755
|
-
const r =
|
|
21984
|
+
const r = spawnSync3("docker", [
|
|
21756
21985
|
"compose",
|
|
21757
21986
|
"-p",
|
|
21758
21987
|
"switchroom",
|
|
@@ -21895,7 +22124,7 @@ class HostdServer {
|
|
|
21895
22124
|
runSwitchroom(args) {
|
|
21896
22125
|
return new Promise((resolve6, reject) => {
|
|
21897
22126
|
const bin = this.opts.switchroomBin ?? "switchroom";
|
|
21898
|
-
const child =
|
|
22127
|
+
const child = spawn2(bin, args, {
|
|
21899
22128
|
stdio: ["ignore", "pipe", "pipe"],
|
|
21900
22129
|
env: { ...process.env }
|
|
21901
22130
|
});
|
|
@@ -21917,6 +22146,49 @@ function tail(s, bytes = TAIL_BYTES) {
|
|
|
21917
22146
|
return s;
|
|
21918
22147
|
return s.slice(s.length - bytes);
|
|
21919
22148
|
}
|
|
22149
|
+
var SCHEDULE_PROMPT_MAX_CHARS = 160;
|
|
22150
|
+
var SCHEDULE_OUTPUT_SUMMARY_MAX_CHARS = 100;
|
|
22151
|
+
var SCHEDULE_MAX_FIRES_PER_AGENT = 8;
|
|
22152
|
+
var SCHEDULE_FRAME_BUDGET_BYTES = MAX_FRAME_BYTES - 2048;
|
|
22153
|
+
function slimScheduleEntry(e) {
|
|
22154
|
+
const slim = {
|
|
22155
|
+
agent: e.agent,
|
|
22156
|
+
scheduleIndex: e.scheduleIndex,
|
|
22157
|
+
cron: e.cron,
|
|
22158
|
+
promptKey: e.promptKey
|
|
22159
|
+
};
|
|
22160
|
+
if (e.prompt !== undefined) {
|
|
22161
|
+
slim.prompt = e.prompt.length > SCHEDULE_PROMPT_MAX_CHARS ? e.prompt.slice(0, SCHEDULE_PROMPT_MAX_CHARS) : e.prompt;
|
|
22162
|
+
}
|
|
22163
|
+
if (e.kind !== undefined)
|
|
22164
|
+
slim.kind = e.kind;
|
|
22165
|
+
if (e.model !== undefined)
|
|
22166
|
+
slim.model = e.model;
|
|
22167
|
+
if (e.context !== undefined)
|
|
22168
|
+
slim.context = e.context;
|
|
22169
|
+
if (e.topic !== undefined)
|
|
22170
|
+
slim.topic = e.topic;
|
|
22171
|
+
return slim;
|
|
22172
|
+
}
|
|
22173
|
+
function scheduleFrameBytes(view) {
|
|
22174
|
+
return Buffer.byteLength(JSON.stringify(JSON.stringify(view)), "utf8");
|
|
22175
|
+
}
|
|
22176
|
+
function boundScheduleView(entries, recentByAgent) {
|
|
22177
|
+
const slimEntries = entries.map(slimScheduleEntry);
|
|
22178
|
+
const boundedRecent = {};
|
|
22179
|
+
for (const [agent, rows] of Object.entries(recentByAgent)) {
|
|
22180
|
+
boundedRecent[agent] = rows.slice(-SCHEDULE_MAX_FIRES_PER_AGENT).map((r) => typeof r.outputSummary === "string" && r.outputSummary.length > SCHEDULE_OUTPUT_SUMMARY_MAX_CHARS ? { ...r, outputSummary: r.outputSummary.slice(0, SCHEDULE_OUTPUT_SUMMARY_MAX_CHARS) } : { ...r });
|
|
22181
|
+
}
|
|
22182
|
+
let view = { entries: slimEntries, recentByAgent: boundedRecent };
|
|
22183
|
+
if (scheduleFrameBytes(view) <= SCHEDULE_FRAME_BUDGET_BYTES)
|
|
22184
|
+
return view;
|
|
22185
|
+
view = { entries: slimEntries, recentByAgent: {}, truncated: true };
|
|
22186
|
+
while (scheduleFrameBytes(view) > SCHEDULE_FRAME_BUDGET_BYTES && view.entries.length > 1) {
|
|
22187
|
+
const keep = Math.max(1, Math.floor(view.entries.length * 0.8));
|
|
22188
|
+
view = { entries: view.entries.slice(0, keep), recentByAgent: {}, truncated: true };
|
|
22189
|
+
}
|
|
22190
|
+
return view;
|
|
22191
|
+
}
|
|
21920
22192
|
var READONLY_EXEC_ALLOWLIST = [
|
|
21921
22193
|
"cat",
|
|
21922
22194
|
"df",
|
|
@@ -22199,10 +22471,10 @@ function errMsg(err2) {
|
|
|
22199
22471
|
}
|
|
22200
22472
|
|
|
22201
22473
|
// src/host-control/release-watcher-shellouts.ts
|
|
22202
|
-
import { spawn as
|
|
22474
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
22203
22475
|
async function run(cmd, args, timeoutMs) {
|
|
22204
22476
|
return new Promise((res) => {
|
|
22205
|
-
const p =
|
|
22477
|
+
const p = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
22206
22478
|
let stdout = "";
|
|
22207
22479
|
let stderr = "";
|
|
22208
22480
|
const to = setTimeout(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.27",
|
|
4
4
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,12 +27,13 @@
|
|
|
27
27
|
"test:vitest": "vitest run",
|
|
28
28
|
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
|
|
29
29
|
"test:watch": "vitest",
|
|
30
|
-
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs",
|
|
30
|
+
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
|
|
31
31
|
"lint:tsc": "tsc --noEmit",
|
|
32
32
|
"lint:plugin-references": "node scripts/check-plugin-references.mjs",
|
|
33
33
|
"lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
|
|
34
34
|
"lint:bun-test-imports": "node scripts/check-bun-test-imports.mjs",
|
|
35
35
|
"lint:no-pii": "node scripts/check-no-pii-secrets.mjs",
|
|
36
|
+
"lint:web-subscription-honest": "node scripts/check-web-subscription-honest.mjs",
|
|
36
37
|
"lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
|
|
37
38
|
"prepublishOnly": "npm run build && npm run lint && npm test"
|
|
38
39
|
},
|
|
@@ -46442,6 +46442,20 @@ var DoctorRequestSchema = exports_external.object({
|
|
|
46442
46442
|
op: exports_external.literal("doctor"),
|
|
46443
46443
|
args: exports_external.object({}).optional()
|
|
46444
46444
|
});
|
|
46445
|
+
var AgentStatusRequestSchema = exports_external.object({
|
|
46446
|
+
...RequestEnvelope,
|
|
46447
|
+
op: exports_external.literal("agent_status"),
|
|
46448
|
+
args: exports_external.object({
|
|
46449
|
+
name: AgentNameSchema2.optional()
|
|
46450
|
+
})
|
|
46451
|
+
});
|
|
46452
|
+
var AgentScheduleRequestSchema = exports_external.object({
|
|
46453
|
+
...RequestEnvelope,
|
|
46454
|
+
op: exports_external.literal("agent_schedule"),
|
|
46455
|
+
args: exports_external.object({
|
|
46456
|
+
name: AgentNameSchema2.optional()
|
|
46457
|
+
})
|
|
46458
|
+
});
|
|
46445
46459
|
var AgentSmokeRequestSchema = exports_external.object({
|
|
46446
46460
|
...RequestEnvelope,
|
|
46447
46461
|
op: exports_external.literal("agent_smoke"),
|
|
@@ -46472,6 +46486,8 @@ var RequestSchema3 = exports_external.discriminatedUnion("op", [
|
|
|
46472
46486
|
AgentExecRequestSchema,
|
|
46473
46487
|
DoctorRequestSchema,
|
|
46474
46488
|
AgentSmokeRequestSchema,
|
|
46489
|
+
AgentStatusRequestSchema,
|
|
46490
|
+
AgentScheduleRequestSchema,
|
|
46475
46491
|
ConfigProposeEditRequestSchema
|
|
46476
46492
|
]);
|
|
46477
46493
|
var ResultSchema = exports_external.enum(["started", "completed", "denied", "error"]);
|
|
@@ -46523,6 +46539,7 @@ var ResponseEnvelope = {
|
|
|
46523
46539
|
audit_id: exports_external.string().min(1).optional(),
|
|
46524
46540
|
stdout_tail: exports_external.string().optional(),
|
|
46525
46541
|
stderr_tail: exports_external.string().optional(),
|
|
46542
|
+
payload: exports_external.string().optional(),
|
|
46526
46543
|
error: exports_external.string().optional(),
|
|
46527
46544
|
error_envelope: ErrorEnvelopeSchema.optional()
|
|
46528
46545
|
};
|
|
@@ -54424,10 +54441,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
54424
54441
|
}
|
|
54425
54442
|
|
|
54426
54443
|
// ../src/build-info.ts
|
|
54427
|
-
var VERSION = "0.15.
|
|
54428
|
-
var COMMIT_SHA = "
|
|
54429
|
-
var COMMIT_DATE = "2026-06-
|
|
54430
|
-
var LATEST_PR =
|
|
54444
|
+
var VERSION = "0.15.27";
|
|
54445
|
+
var COMMIT_SHA = "97160057";
|
|
54446
|
+
var COMMIT_DATE = "2026-06-15T06:43:23Z";
|
|
54447
|
+
var LATEST_PR = 2372;
|
|
54431
54448
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
54432
54449
|
|
|
54433
54450
|
// gateway/boot-version.ts
|
|
@@ -54720,6 +54737,112 @@ async function revokeGrantViaBroker(id, opts) {
|
|
|
54720
54737
|
|
|
54721
54738
|
// gateway/linear-activity.ts
|
|
54722
54739
|
init_client2();
|
|
54740
|
+
|
|
54741
|
+
// ../src/linear/oauth-refresh.ts
|
|
54742
|
+
var LINEAR_TOKEN_ENDPOINT = "https://api.linear.app/oauth/token";
|
|
54743
|
+
var DEFAULT_REFRESH_SKEW_SEC = 2 * 3600;
|
|
54744
|
+
async function refreshLinearAppToken(bundle, opts = {}) {
|
|
54745
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
54746
|
+
const nowSec = opts.nowSec ?? (() => Math.floor(Date.now() / 1000));
|
|
54747
|
+
const form = new URLSearchParams({
|
|
54748
|
+
grant_type: "refresh_token",
|
|
54749
|
+
refresh_token: bundle.refreshToken,
|
|
54750
|
+
client_id: bundle.clientId,
|
|
54751
|
+
client_secret: bundle.clientSecret
|
|
54752
|
+
});
|
|
54753
|
+
let resp;
|
|
54754
|
+
try {
|
|
54755
|
+
resp = await fetchImpl(LINEAR_TOKEN_ENDPOINT, {
|
|
54756
|
+
method: "POST",
|
|
54757
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
54758
|
+
body: form.toString()
|
|
54759
|
+
});
|
|
54760
|
+
} catch (err) {
|
|
54761
|
+
return { ok: false, reason: "network", detail: err.message };
|
|
54762
|
+
}
|
|
54763
|
+
if (!resp.ok) {
|
|
54764
|
+
const txt = await resp.text().catch(() => "");
|
|
54765
|
+
const revoked = resp.status === 400 || /invalid_grant|invalid_token/i.test(txt);
|
|
54766
|
+
return {
|
|
54767
|
+
ok: false,
|
|
54768
|
+
reason: revoked ? "revoked" : "http_error",
|
|
54769
|
+
detail: `HTTP ${resp.status}${txt ? ` ${txt.slice(0, 200)}` : ""}`
|
|
54770
|
+
};
|
|
54771
|
+
}
|
|
54772
|
+
let json;
|
|
54773
|
+
try {
|
|
54774
|
+
json = await resp.json();
|
|
54775
|
+
} catch {
|
|
54776
|
+
return { ok: false, reason: "bad_response", detail: "non-JSON token response" };
|
|
54777
|
+
}
|
|
54778
|
+
const accessToken = json.access_token;
|
|
54779
|
+
if (typeof accessToken !== "string" || accessToken.length === 0) {
|
|
54780
|
+
return { ok: false, reason: "bad_response", detail: "no access_token in response" };
|
|
54781
|
+
}
|
|
54782
|
+
const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 86400;
|
|
54783
|
+
const rotated = typeof json.refresh_token === "string" && json.refresh_token.length > 0 ? json.refresh_token : bundle.refreshToken;
|
|
54784
|
+
return {
|
|
54785
|
+
ok: true,
|
|
54786
|
+
accessToken,
|
|
54787
|
+
refreshToken: rotated,
|
|
54788
|
+
expiresAt: nowSec() + expiresIn,
|
|
54789
|
+
...typeof json.scope === "string" ? { scope: json.scope } : {}
|
|
54790
|
+
};
|
|
54791
|
+
}
|
|
54792
|
+
function parseBundle(raw) {
|
|
54793
|
+
if (raw == null || raw === "")
|
|
54794
|
+
return null;
|
|
54795
|
+
let o;
|
|
54796
|
+
try {
|
|
54797
|
+
o = JSON.parse(raw);
|
|
54798
|
+
} catch {
|
|
54799
|
+
return null;
|
|
54800
|
+
}
|
|
54801
|
+
if (typeof o.client_id === "string" && typeof o.client_secret === "string" && typeof o.refresh_token === "string" && o.client_id.length > 0 && o.client_secret.length > 0 && o.refresh_token.length > 0) {
|
|
54802
|
+
return {
|
|
54803
|
+
clientId: o.client_id,
|
|
54804
|
+
clientSecret: o.client_secret,
|
|
54805
|
+
refreshToken: o.refresh_token,
|
|
54806
|
+
...typeof o.expires_at === "number" ? { expiresAt: o.expires_at } : {}
|
|
54807
|
+
};
|
|
54808
|
+
}
|
|
54809
|
+
return null;
|
|
54810
|
+
}
|
|
54811
|
+
function serializeBundle(b) {
|
|
54812
|
+
return JSON.stringify({
|
|
54813
|
+
client_id: b.clientId,
|
|
54814
|
+
client_secret: b.clientSecret,
|
|
54815
|
+
refresh_token: b.refreshToken,
|
|
54816
|
+
...b.expiresAt != null ? { expires_at: b.expiresAt } : {}
|
|
54817
|
+
});
|
|
54818
|
+
}
|
|
54819
|
+
async function performLinearRefresh(io) {
|
|
54820
|
+
const raw = await io.readBundle();
|
|
54821
|
+
const bundle = parseBundle(raw);
|
|
54822
|
+
if (!bundle) {
|
|
54823
|
+
return { ok: false, reason: "no_bundle", detail: "no/invalid refresh bundle" };
|
|
54824
|
+
}
|
|
54825
|
+
const res = await refreshLinearAppToken(bundle, {
|
|
54826
|
+
...io.fetchImpl ? { fetchImpl: io.fetchImpl } : {},
|
|
54827
|
+
...io.nowSec ? { nowSec: io.nowSec } : {}
|
|
54828
|
+
});
|
|
54829
|
+
if (!res.ok)
|
|
54830
|
+
return { ok: false, reason: res.reason, detail: res.detail };
|
|
54831
|
+
try {
|
|
54832
|
+
await io.writeBundle(serializeBundle({
|
|
54833
|
+
clientId: bundle.clientId,
|
|
54834
|
+
clientSecret: bundle.clientSecret,
|
|
54835
|
+
refreshToken: res.refreshToken,
|
|
54836
|
+
expiresAt: res.expiresAt
|
|
54837
|
+
}));
|
|
54838
|
+
await io.writeToken(res.accessToken);
|
|
54839
|
+
} catch (err) {
|
|
54840
|
+
return { ok: false, reason: "persist_failed", detail: err.message };
|
|
54841
|
+
}
|
|
54842
|
+
return { ok: true, accessToken: res.accessToken, expiresAt: res.expiresAt };
|
|
54843
|
+
}
|
|
54844
|
+
|
|
54845
|
+
// gateway/linear-activity.ts
|
|
54723
54846
|
var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
|
|
54724
54847
|
async function defaultResolveLinearToken(agent) {
|
|
54725
54848
|
const key = `linear/${agent}/token`;
|
|
@@ -54736,6 +54859,53 @@ async function defaultResolveLinearToken(agent) {
|
|
|
54736
54859
|
return { ok: false, reason: "denied" };
|
|
54737
54860
|
return { ok: false, reason: "unknown" };
|
|
54738
54861
|
}
|
|
54862
|
+
function brokerRefreshIO(agent, fetchImpl) {
|
|
54863
|
+
const token = readVaultTokenFile(agent) ?? undefined;
|
|
54864
|
+
const opt = token ? { token } : {};
|
|
54865
|
+
return {
|
|
54866
|
+
readBundle: async () => {
|
|
54867
|
+
const r = await getViaBrokerStructured(`linear/${agent}/oauth`, opt);
|
|
54868
|
+
return r.kind === "ok" && r.entry.kind === "string" ? r.entry.value : null;
|
|
54869
|
+
},
|
|
54870
|
+
writeToken: async (t) => {
|
|
54871
|
+
const r = await putViaBroker(`linear/${agent}/token`, { kind: "string", value: t }, opt);
|
|
54872
|
+
if (r.kind !== "ok")
|
|
54873
|
+
throw new Error(`broker put linear/${agent}/token: ${r.kind}`);
|
|
54874
|
+
},
|
|
54875
|
+
writeBundle: async (j) => {
|
|
54876
|
+
const r = await putViaBroker(`linear/${agent}/oauth`, { kind: "string", value: j }, opt);
|
|
54877
|
+
if (r.kind !== "ok")
|
|
54878
|
+
throw new Error(`broker put linear/${agent}/oauth: ${r.kind}`);
|
|
54879
|
+
},
|
|
54880
|
+
...fetchImpl ? { fetchImpl } : {}
|
|
54881
|
+
};
|
|
54882
|
+
}
|
|
54883
|
+
async function linearPostWithRefresh(body, token, agent, fetchImpl, log, refreshIO) {
|
|
54884
|
+
const post = (t) => fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
|
|
54885
|
+
method: "POST",
|
|
54886
|
+
headers: { "Content-Type": "application/json", Authorization: t },
|
|
54887
|
+
body
|
|
54888
|
+
});
|
|
54889
|
+
let resp = await post(token);
|
|
54890
|
+
if (resp.status !== 401)
|
|
54891
|
+
return { resp, token };
|
|
54892
|
+
const io = (refreshIO ?? ((a) => brokerRefreshIO(a, fetchImpl)))(agent);
|
|
54893
|
+
const refreshed = await performLinearRefresh({ ...io, fetchImpl });
|
|
54894
|
+
if (!refreshed.ok) {
|
|
54895
|
+
if (refreshed.reason === "revoked") {
|
|
54896
|
+
log(`telegram gateway: linear token REVOKED agent=${agent} \u2014 refresh token is dead; ` + `operator must re-authorize (linear-agent setup --refresh-token \u2026)
|
|
54897
|
+
`);
|
|
54898
|
+
} else {
|
|
54899
|
+
log(`telegram gateway: linear token refresh failed agent=${agent} reason=${refreshed.reason}
|
|
54900
|
+
`);
|
|
54901
|
+
}
|
|
54902
|
+
return { resp, token };
|
|
54903
|
+
}
|
|
54904
|
+
log(`telegram gateway: linear token auto-refreshed agent=${agent} (was 401)
|
|
54905
|
+
`);
|
|
54906
|
+
resp = await post(refreshed.accessToken);
|
|
54907
|
+
return { resp, token: refreshed.accessToken };
|
|
54908
|
+
}
|
|
54739
54909
|
async function emitLinearAgentActivity(args, deps = {}) {
|
|
54740
54910
|
const log = deps.log ?? ((s) => process.stderr.write(s));
|
|
54741
54911
|
const sessionId = args.agent_session_id;
|
|
@@ -54780,14 +54950,7 @@ async function emitLinearAgentActivity(args, deps = {}) {
|
|
|
54780
54950
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
54781
54951
|
let resp;
|
|
54782
54952
|
try {
|
|
54783
|
-
resp = await
|
|
54784
|
-
method: "POST",
|
|
54785
|
-
headers: {
|
|
54786
|
-
"Content-Type": "application/json",
|
|
54787
|
-
Authorization: tokenResult.token
|
|
54788
|
-
},
|
|
54789
|
-
body: JSON.stringify({ query: mutation, variables })
|
|
54790
|
-
});
|
|
54953
|
+
({ resp } = await linearPostWithRefresh(JSON.stringify({ query: mutation, variables }), tokenResult.token, agent, fetchImpl, log, deps.refreshIO));
|
|
54791
54954
|
} catch (err) {
|
|
54792
54955
|
return {
|
|
54793
54956
|
content: [{ type: "text", text: `linear_agent_activity failed: request error: ${err.message}` }]
|
|
@@ -54847,15 +55010,13 @@ async function createLinearIssue(args, deps = {}) {
|
|
|
54847
55010
|
]
|
|
54848
55011
|
};
|
|
54849
55012
|
}
|
|
54850
|
-
|
|
55013
|
+
let activeToken = tokenResult.token;
|
|
54851
55014
|
const gql = async (query2, variables) => {
|
|
54852
55015
|
let resp;
|
|
54853
55016
|
try {
|
|
54854
|
-
|
|
54855
|
-
|
|
54856
|
-
|
|
54857
|
-
body: JSON.stringify({ query: query2, variables })
|
|
54858
|
-
});
|
|
55017
|
+
const out = await linearPostWithRefresh(JSON.stringify({ query: query2, variables }), activeToken, agent, fetchImpl, log, deps.refreshIO);
|
|
55018
|
+
resp = out.resp;
|
|
55019
|
+
activeToken = out.token;
|
|
54859
55020
|
} catch (err) {
|
|
54860
55021
|
return { ok: false, text: `request error: ${err.message}` };
|
|
54861
55022
|
}
|