starbridge 0.1.0 → 0.1.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/README.md +32 -6
- package/dist/starbridge.js +859 -222
- package/package.json +1 -1
package/dist/starbridge.js
CHANGED
|
@@ -4056,7 +4056,7 @@ var require_png = __commonJS(function(exports) {
|
|
|
4056
4056
|
});
|
|
4057
4057
|
|
|
4058
4058
|
// src/cli.ts
|
|
4059
|
-
import { readFileSync as
|
|
4059
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
4060
4060
|
import { parseArgs } from "node:util";
|
|
4061
4061
|
|
|
4062
4062
|
// ../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js
|
|
@@ -9651,7 +9651,7 @@ function duration2(params) {
|
|
|
9651
9651
|
}
|
|
9652
9652
|
// ../packages/protocol/src/client.ts
|
|
9653
9653
|
var CLIENT_HEADER = "starbridge-client";
|
|
9654
|
-
var CLIENT_NAMES = ["cli", "android", "web", "mod"];
|
|
9654
|
+
var CLIENT_NAMES = ["cli", "android", "web", "mod", "desktop"];
|
|
9655
9655
|
function clientHeader(name, version) {
|
|
9656
9656
|
return `${name}/${version}`;
|
|
9657
9657
|
}
|
|
@@ -13901,30 +13901,65 @@ function activeMembers(dir, role) {
|
|
|
13901
13901
|
return [...dir.members.values()].filter((e) => e.active && e.member.role === role).map((e) => e.member);
|
|
13902
13902
|
}
|
|
13903
13903
|
// ../packages/protocol/src/heads.ts
|
|
13904
|
+
function revocationOf(entries, id) {
|
|
13905
|
+
for (const raw of entries) {
|
|
13906
|
+
const env = SignedEnvelope.safeParse(raw);
|
|
13907
|
+
if (!env.success)
|
|
13908
|
+
continue;
|
|
13909
|
+
const body = JSON.parse(env.data.body);
|
|
13910
|
+
if (body.op === "revoke" && body.id === id)
|
|
13911
|
+
return { id, by: env.data.signer, at: body.at ?? "" };
|
|
13912
|
+
}
|
|
13913
|
+
return;
|
|
13914
|
+
}
|
|
13915
|
+
function removed(dir, entries, id) {
|
|
13916
|
+
if (dir.members.get(id)?.active !== false)
|
|
13917
|
+
return false;
|
|
13918
|
+
let revokedAt = -1;
|
|
13919
|
+
let recoveredAt = -1;
|
|
13920
|
+
entries.forEach((raw, i) => {
|
|
13921
|
+
const env = SignedEnvelope.safeParse(raw);
|
|
13922
|
+
if (!env.success)
|
|
13923
|
+
return;
|
|
13924
|
+
const body = JSON.parse(env.data.body);
|
|
13925
|
+
if (body.op === "revoke" && body.id === id)
|
|
13926
|
+
revokedAt = i;
|
|
13927
|
+
if (body.op === "recover")
|
|
13928
|
+
recoveredAt = i;
|
|
13929
|
+
});
|
|
13930
|
+
return recoveredAt > revokedAt;
|
|
13931
|
+
}
|
|
13904
13932
|
function noteHead(heads, signer, head, entries, dir) {
|
|
13905
13933
|
if (!head)
|
|
13906
13934
|
return false;
|
|
13907
13935
|
const by = head.by && head.by !== signer ? head.by : undefined;
|
|
13936
|
+
if (by && dir?.members.get(by)?.active === false)
|
|
13937
|
+
return false;
|
|
13908
13938
|
const key = !by ? signer : dir && !dir.members.has(by) ? `${signer}/?` : `${signer}/${by}`;
|
|
13909
13939
|
const known = heads[key];
|
|
13910
|
-
const dropped = !!known?.by && dir
|
|
13940
|
+
const dropped = !!known?.by && !!dir && removed(dir, entries, known.by);
|
|
13911
13941
|
const replace = !known || dropped || head.length > known.length || holdsHead(entries, known) && !holdsHead(entries, head);
|
|
13912
13942
|
if (replace)
|
|
13913
13943
|
heads[key] = head;
|
|
13914
13944
|
return replace;
|
|
13915
13945
|
}
|
|
13916
13946
|
function withheldBy(heads, dir, entries) {
|
|
13947
|
+
let fork;
|
|
13917
13948
|
for (const [key, head] of Object.entries(heads)) {
|
|
13918
13949
|
const id = key.split("/")[0];
|
|
13919
13950
|
const by = head.by && head.by !== id ? head.by : undefined;
|
|
13920
|
-
if (!dir.members.
|
|
13951
|
+
if (!dir.members.has(id) || removed(dir, entries, id))
|
|
13921
13952
|
continue;
|
|
13922
|
-
if (by && dir
|
|
13953
|
+
if (by && removed(dir, entries, by))
|
|
13923
13954
|
continue;
|
|
13924
|
-
if (
|
|
13955
|
+
if (holdsHead(entries, head))
|
|
13956
|
+
continue;
|
|
13957
|
+
const revoked = (dir.members.get(id)?.active ? undefined : revocationOf(entries, id)) ?? (by && dir.members.get(by)?.active === false ? revocationOf(entries, by) : undefined);
|
|
13958
|
+
if (!revoked)
|
|
13925
13959
|
return { id, ...by ? { by } : {}, head };
|
|
13960
|
+
fork ??= { id, ...by ? { by } : {}, head, revoked };
|
|
13926
13961
|
}
|
|
13927
|
-
return;
|
|
13962
|
+
return fork;
|
|
13928
13963
|
}
|
|
13929
13964
|
// ../packages/protocol/src/images.ts
|
|
13930
13965
|
var label = concat(utf8("starbridge/v1/image"), new Uint8Array([0]));
|
|
@@ -13966,7 +14001,19 @@ function formatPairingCode(code) {
|
|
|
13966
14001
|
return (code.rendezvous + code.secret).match(/.{4}/g)?.join("-") ?? "";
|
|
13967
14002
|
}
|
|
13968
14003
|
function pairingLink(server, code) {
|
|
13969
|
-
|
|
14004
|
+
let end = server.length;
|
|
14005
|
+
while (end > 0 && server[end - 1] === "/")
|
|
14006
|
+
end--;
|
|
14007
|
+
return `${server.slice(0, end)}/pair#${formatPairingCode(code)}`;
|
|
14008
|
+
}
|
|
14009
|
+
function appPairingLink(server, code, checkKey) {
|
|
14010
|
+
let end = server.length;
|
|
14011
|
+
while (end > 0 && server[end - 1] === "/")
|
|
14012
|
+
end--;
|
|
14013
|
+
return `starbridge://pair?server=${encodeURIComponent(server.slice(0, end))}&k=${checkKey}#${formatPairingCode(code)}`;
|
|
14014
|
+
}
|
|
14015
|
+
function newCheckKey() {
|
|
14016
|
+
return encodeCrockford(libsodium_wrappers_default.randombytes_buf(10));
|
|
13970
14017
|
}
|
|
13971
14018
|
function newClaimSecret() {
|
|
13972
14019
|
return toB64(libsodium_wrappers_default.randombytes_buf(32));
|
|
@@ -13993,7 +14040,8 @@ var PairingApprovalBody = object({
|
|
|
13993
14040
|
account: Id,
|
|
13994
14041
|
length: number2().int().positive(),
|
|
13995
14042
|
head: B64,
|
|
13996
|
-
approver: Id
|
|
14043
|
+
approver: Id,
|
|
14044
|
+
check: B64.optional()
|
|
13997
14045
|
});
|
|
13998
14046
|
var PairingMessage = object({ body: string2(), mac: B64 });
|
|
13999
14047
|
function macMessage(kind, body) {
|
|
@@ -14035,6 +14083,33 @@ function checkJoined(dir, me) {
|
|
|
14035
14083
|
if (!entry?.active || entry.member.role !== me.role || entry.member.boxPk !== me.boxPk || entry.member.signPk !== me.signPk)
|
|
14036
14084
|
throw new ProtocolError("unknown-member", "the directory does not hold my keys");
|
|
14037
14085
|
}
|
|
14086
|
+
function checkInput(entries, id) {
|
|
14087
|
+
const nul = new Uint8Array([0]);
|
|
14088
|
+
for (const e of entries) {
|
|
14089
|
+
const { body, sig } = e ?? {};
|
|
14090
|
+
if (typeof body !== "string" || typeof sig !== "string")
|
|
14091
|
+
continue;
|
|
14092
|
+
const parsed = JSON.parse(body);
|
|
14093
|
+
if (parsed.op !== "add" || parsed.member?.id !== id)
|
|
14094
|
+
continue;
|
|
14095
|
+
return concat(utf8("starbridge/v1/check"), nul, utf8(body), nul, utf8(sig));
|
|
14096
|
+
}
|
|
14097
|
+
throw new ProtocolError("unknown-member", `no add entry for ${id}`);
|
|
14098
|
+
}
|
|
14099
|
+
function checkCode(entries, id) {
|
|
14100
|
+
const hash = libsodium_wrappers_default.crypto_generichash(32, checkInput(entries, id), null);
|
|
14101
|
+
return encodeCrockford(hash.subarray(0, 10)).match(/.{4}/g).join("-");
|
|
14102
|
+
}
|
|
14103
|
+
function checkKeyBytes(key) {
|
|
14104
|
+
return libsodium_wrappers_default.crypto_generichash(32, concat(utf8("starbridge/v1/check-key"), new Uint8Array([0]), utf8(key)), null);
|
|
14105
|
+
}
|
|
14106
|
+
function verifyCheckProof(entries, id, key, proof) {
|
|
14107
|
+
try {
|
|
14108
|
+
return libsodium_wrappers_default.crypto_auth_verify(fromB64(proof), checkInput(entries, id), checkKeyBytes(key));
|
|
14109
|
+
} catch {
|
|
14110
|
+
return false;
|
|
14111
|
+
}
|
|
14112
|
+
}
|
|
14038
14113
|
|
|
14039
14114
|
// ../packages/protocol/src/join.ts
|
|
14040
14115
|
var JoinRequestBody = object({
|
|
@@ -14171,6 +14246,17 @@ function checkPermissionAnswer(asked, answer, device, now = Date.now()) {
|
|
|
14171
14246
|
if (now > Date.parse(asked.expiresAt))
|
|
14172
14247
|
throw new ProtocolError("expired");
|
|
14173
14248
|
}
|
|
14249
|
+
// ../packages/protocol/src/presence.ts
|
|
14250
|
+
var PRESENCE_INPUT_MS = 60000;
|
|
14251
|
+
var PRESENCE_BEAT_MS = 30000;
|
|
14252
|
+
var Presence = object({ present: boolean2() });
|
|
14253
|
+
var PUSH_HOLD_MAX = 300;
|
|
14254
|
+
var AccountSettings = object({
|
|
14255
|
+
pushHold: number2().int().min(0).max(PUSH_HOLD_MAX)
|
|
14256
|
+
});
|
|
14257
|
+
function isPresent(screen) {
|
|
14258
|
+
return !screen.locked && screen.idleMs >= 0 && screen.idleMs < PRESENCE_INPUT_MS;
|
|
14259
|
+
}
|
|
14174
14260
|
// src/agent/client.ts
|
|
14175
14261
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
14176
14262
|
import { existsSync as existsSync2 } from "node:fs";
|
|
@@ -14273,7 +14359,7 @@ function processAlive(pid) {
|
|
|
14273
14359
|
// package.json
|
|
14274
14360
|
var package_default = {
|
|
14275
14361
|
name: "starbridge",
|
|
14276
|
-
version: "0.1.
|
|
14362
|
+
version: "0.1.1",
|
|
14277
14363
|
description: "Post decisions to your Starbridge devices and upload quota windows from CodexBar",
|
|
14278
14364
|
license: "MIT",
|
|
14279
14365
|
repository: {
|
|
@@ -14344,8 +14430,9 @@ function configDir(env) {
|
|
|
14344
14430
|
}
|
|
14345
14431
|
var PROMPTS_OPEN = "permissions-open";
|
|
14346
14432
|
var CLI_PATH = "cli-path";
|
|
14433
|
+
var PICKER_MAX_MS = 86400000;
|
|
14347
14434
|
function promptsMark(s, now = Date.now()) {
|
|
14348
|
-
const open = Object.values(s.permissions ?? {}).some((p) => !p.settled && Date.parse(p.permission.expiresAt) > now);
|
|
14435
|
+
const open = Object.values(s.permissions ?? {}).some((p) => !p.settled && Date.parse(p.permission.expiresAt) > now) || Object.entries(s.asked).some(([id, a]) => a.picker && !a.settled && !s.answers[id] && Date.parse(a.askedAt) > now - PICKER_MAX_MS);
|
|
14349
14436
|
return open ? "open" : "";
|
|
14350
14437
|
}
|
|
14351
14438
|
var STATE_VERSION = 1;
|
|
@@ -14800,6 +14887,9 @@ class Api {
|
|
|
14800
14887
|
const r = await this.call("GET", "/quota/senders", { signal: AbortSignal.timeout(15000) });
|
|
14801
14888
|
return r.json.senders;
|
|
14802
14889
|
}
|
|
14890
|
+
async presence(present, signal) {
|
|
14891
|
+
await this.call("PUT", "/presence", { body: { present }, ...signal ? { signal } : {} });
|
|
14892
|
+
}
|
|
14803
14893
|
async postItem(item, signal) {
|
|
14804
14894
|
await this.call("POST", "/items", { body: item, ...signal ? { signal } : {} });
|
|
14805
14895
|
}
|
|
@@ -14865,20 +14955,12 @@ function signedHead(dir) {
|
|
|
14865
14955
|
return { length: dir.length, head: dir.head };
|
|
14866
14956
|
}
|
|
14867
14957
|
function withheld(st, dir, entries) {
|
|
14868
|
-
const
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
return `${id} signed a chain this machine does not hold (${head.length}), and this machine's chain revokes it: the server may be serving a fork. If you revoked ${id} because it was compromised, run \`starbridge pair --force\``;
|
|
14875
|
-
return;
|
|
14876
|
-
}
|
|
14877
|
-
function revokes(entries, id) {
|
|
14878
|
-
return entries.some((e) => {
|
|
14879
|
-
const body = JSON.parse(e.body);
|
|
14880
|
-
return body.op === "revoke" && body.id === id;
|
|
14881
|
-
});
|
|
14958
|
+
const held = withheldBy(st.heads ?? {}, dir, entries);
|
|
14959
|
+
if (!held)
|
|
14960
|
+
return;
|
|
14961
|
+
if (held.revoked)
|
|
14962
|
+
return `${held.id} signed a chain this machine does not hold (${held.head.length}), and this machine's chain revokes it: the server may be serving a fork. If you revoked ${held.id} because it was compromised, run \`starbridge pair --force\``;
|
|
14963
|
+
return `the server is holding back directory entries ${held.id} has seen (${held.head.length}, this machine has ${dir.length})`;
|
|
14882
14964
|
}
|
|
14883
14965
|
function devices(ctx, dir) {
|
|
14884
14966
|
const behind = withheld(ctx.store.state(), dir, ctx.store.directory().slice(0, dir.length));
|
|
@@ -15621,6 +15703,7 @@ function prune(st, now) {
|
|
|
15621
15703
|
if (Date.parse(p.permission.expiresAt) + KEEP_MS < now)
|
|
15622
15704
|
delete st.permissions?.[id];
|
|
15623
15705
|
}
|
|
15706
|
+
var LATE_REPORT_MS = 5000;
|
|
15624
15707
|
async function postPermission(ctx, s, hook, opts, signal) {
|
|
15625
15708
|
const dir = await refreshDirectory(ctx, s, signal);
|
|
15626
15709
|
const to = devices(ctx, dir);
|
|
@@ -15648,7 +15731,9 @@ async function postPermission(ctx, s, hook, opts, signal) {
|
|
|
15648
15731
|
await s.api.postItem(item, signal);
|
|
15649
15732
|
} catch (e) {
|
|
15650
15733
|
const keyboard = signal?.aborted && signal.reason?.name !== "TimeoutError";
|
|
15651
|
-
markSettled(ctx, permission.id, keyboard ? "keyboard" : "timeout");
|
|
15734
|
+
const how = markSettled(ctx, permission.id, keyboard ? "keyboard" : "timeout");
|
|
15735
|
+
if (how)
|
|
15736
|
+
await postSettled(ctx, s, permission.id, how, AbortSignal.timeout(LATE_REPORT_MS)).catch(() => {});
|
|
15652
15737
|
throw e;
|
|
15653
15738
|
}
|
|
15654
15739
|
return permission.id;
|
|
@@ -15871,7 +15956,8 @@ async function postDecision(ctx, s, input) {
|
|
|
15871
15956
|
...decision.done ? { done: true } : {},
|
|
15872
15957
|
...input.codex && decision.source.session ? { codex: input.codex } : {},
|
|
15873
15958
|
...input.extensionAnswers && decision.source.session ? { extensionAnswers: true } : {},
|
|
15874
|
-
...input.held ? { held: true } : {}
|
|
15959
|
+
...input.held ? { held: true } : {},
|
|
15960
|
+
...input.picker ? { picker: input.picker } : {}
|
|
15875
15961
|
};
|
|
15876
15962
|
});
|
|
15877
15963
|
if (input.waiting)
|
|
@@ -16993,6 +17079,11 @@ starbridge run --title 'Mac e2e' --reason 'takes over your screen and keyboard'
|
|
|
16993
17079
|
|
|
16994
17080
|
Wrap the whole command, chained or not. \`--reason\` is required: what it
|
|
16995
17081
|
blocks, or the rule it matches. Output and exit code are the command's own.
|
|
17082
|
+
|
|
17083
|
+
In Codex, ask to run \`starbridge run\` outside the sandbox on the first call
|
|
17084
|
+
(escalated permissions, justified by the network the report needs). In the
|
|
17085
|
+
sandbox the command runs but no device hears of it, and it runs again once
|
|
17086
|
+
the escalation is approved.
|
|
16996
17087
|
`;
|
|
16997
17088
|
|
|
16998
17089
|
// src/setup/marker.ts
|
|
@@ -17027,7 +17118,7 @@ import { configDir } from "./poller.ts";
|
|
|
17027
17118
|
|
|
17028
17119
|
/** The agent API revision this mod speaks, sent in the \`starbridge-api\` header. */
|
|
17029
17120
|
export const API = 1;
|
|
17030
|
-
export const VERSION = "0.1.
|
|
17121
|
+
export const VERSION = "0.1.1";
|
|
17031
17122
|
const CLIENT = \`starbridge-mod/\${VERSION}\`;
|
|
17032
17123
|
|
|
17033
17124
|
/** Event types this mod submits; others are skipped and left unconfirmed, as the API asks. */
|
|
@@ -17817,6 +17908,19 @@ var switch_default = `/**
|
|
|
17817
17908
|
import type { AgentLoop } from "./agent.ts";
|
|
17818
17909
|
import type { Poller } from "./poller.ts";
|
|
17819
17910
|
|
|
17911
|
+
/** Entry points of a run that ends with its last turn: \`claude -p\` and Agent SDK scripts. */
|
|
17912
|
+
const ONE_SHOT = new Set(["sdk-cli", "sdk-ts", "sdk-py"]);
|
|
17913
|
+
|
|
17914
|
+
/**
|
|
17915
|
+
* Whether a session gets an answer loop: one under the REPL, or one a host such as Claude
|
|
17916
|
+
* desktop's Code tab or an IDE keeps open for a person (#863). The engine calls those not
|
|
17917
|
+
* interactive, as it does a \`claude -p\` run, so the host's \`CLAUDE_CODE_ENTRYPOINT\` tells them
|
|
17918
|
+
* apart; a run with none set is \`claude -p\`'s own.
|
|
17919
|
+
*/
|
|
17920
|
+
export function wantsLoop(isInteractive: boolean, entrypoint: string | undefined): boolean {
|
|
17921
|
+
return isInteractive || (!!entrypoint && !ONE_SHOT.has(entrypoint));
|
|
17922
|
+
}
|
|
17923
|
+
|
|
17820
17924
|
export interface Paths {
|
|
17821
17925
|
/** True when an agent answers \`GET /v1/status\` with a 2xx. */
|
|
17822
17926
|
agentUp(): Promise<boolean>;
|
|
@@ -18524,6 +18628,7 @@ function run(sys, cmd, args, opts = {}) {
|
|
|
18524
18628
|
const child = spawn3(start.file, start.args, {
|
|
18525
18629
|
windowsVerbatimArguments: start.windowsVerbatimArguments,
|
|
18526
18630
|
env: { ...sys.ctx.env, ...opts.env },
|
|
18631
|
+
...opts.cwd ? { cwd: opts.cwd } : {},
|
|
18527
18632
|
stdio: [
|
|
18528
18633
|
opts.input === undefined ? "ignore" : "pipe",
|
|
18529
18634
|
opts.inherit ? "inherit" : "pipe",
|
|
@@ -18649,6 +18754,7 @@ var PI_PACKAGE = piSource(VERSION);
|
|
|
18649
18754
|
function hasPi(sys) {
|
|
18650
18755
|
return which(sys.ctx.env, "pi") !== undefined;
|
|
18651
18756
|
}
|
|
18757
|
+
var STARBRIDGE_PI = /github\.com[/:]T0mSIlver\/starbridge(?:\.git)?\/?(?:@.*)?$/i;
|
|
18652
18758
|
function piPackage(sys) {
|
|
18653
18759
|
const dir = sys.ctx.env.PI_CODING_AGENT_DIR || join7(sys.home, ".pi", "agent");
|
|
18654
18760
|
let packages;
|
|
@@ -18661,7 +18767,7 @@ function piPackage(sys) {
|
|
|
18661
18767
|
return;
|
|
18662
18768
|
for (const p of packages) {
|
|
18663
18769
|
const source = typeof p === "string" ? p : p?.source;
|
|
18664
|
-
if (typeof source === "string" &&
|
|
18770
|
+
if (typeof source === "string" && STARBRIDGE_PI.test(source))
|
|
18665
18771
|
return source;
|
|
18666
18772
|
}
|
|
18667
18773
|
return;
|
|
@@ -19561,16 +19667,234 @@ class Permissions {
|
|
|
19561
19667
|
}
|
|
19562
19668
|
}
|
|
19563
19669
|
|
|
19670
|
+
// src/screen.ts
|
|
19671
|
+
import { execFile, spawn as spawn4 } from "node:child_process";
|
|
19672
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
19673
|
+
var TIMEOUT_MS = 5000;
|
|
19674
|
+
var exec = (cmd, args) => new Promise((resolve) => {
|
|
19675
|
+
execFile(cmd, args, { timeout: TIMEOUT_MS, windowsHide: true }, (err, stdout) => resolve(err ? undefined : String(stdout)));
|
|
19676
|
+
});
|
|
19677
|
+
function macIdleMs(ioreg) {
|
|
19678
|
+
const m = /"HIDIdleTime"\s*=\s*(\d+)/.exec(ioreg);
|
|
19679
|
+
return m ? Number(m[1]) / 1e6 : undefined;
|
|
19680
|
+
}
|
|
19681
|
+
function macLocked(ioreg) {
|
|
19682
|
+
return /"CGSSessionScreenIsLocked"\s*=\s*Yes/.test(ioreg);
|
|
19683
|
+
}
|
|
19684
|
+
async function readMac(run) {
|
|
19685
|
+
const [hid, root] = await Promise.all([
|
|
19686
|
+
run("ioreg", ["-c", "IOHIDSystem", "-d", "4", "-r", "-k", "HIDIdleTime"]),
|
|
19687
|
+
run("ioreg", ["-n", "Root", "-d1"])
|
|
19688
|
+
]);
|
|
19689
|
+
const idleMs = hid === undefined ? undefined : macIdleMs(hid);
|
|
19690
|
+
if (idleMs === undefined || root === undefined)
|
|
19691
|
+
return;
|
|
19692
|
+
return { locked: macLocked(root), idleMs };
|
|
19693
|
+
}
|
|
19694
|
+
function properties(text) {
|
|
19695
|
+
const out = {};
|
|
19696
|
+
for (const line of text.split(`
|
|
19697
|
+
`)) {
|
|
19698
|
+
const i = line.indexOf("=");
|
|
19699
|
+
if (i > 0)
|
|
19700
|
+
out[line.slice(0, i)] = line.slice(i + 1).trim();
|
|
19701
|
+
}
|
|
19702
|
+
return out;
|
|
19703
|
+
}
|
|
19704
|
+
function graphical(sessions) {
|
|
19705
|
+
return sessions.map(properties).filter((p) => p.Active === "yes" && (p.Type === "x11" || p.Type === "wayland") && p.Class === "user").map((p) => ({ locked: p.LockedHint === "yes" }));
|
|
19706
|
+
}
|
|
19707
|
+
function linuxIdleMs(text) {
|
|
19708
|
+
const m = /^\(?(?:uint64\s+)?(\d+),?\)?\s*$/.exec(text.trim());
|
|
19709
|
+
return m ? Number(m[1]) : undefined;
|
|
19710
|
+
}
|
|
19711
|
+
async function readLinux(run) {
|
|
19712
|
+
const list = await run("loginctl", ["list-sessions", "--no-legend", "--no-pager"]);
|
|
19713
|
+
const ids = (list ?? "").split(`
|
|
19714
|
+
`).map((l) => l.trim().split(/\s+/)[0]).filter((id) => !!id);
|
|
19715
|
+
const shown = await Promise.all(ids.map((id) => run("loginctl", [
|
|
19716
|
+
"show-session",
|
|
19717
|
+
id,
|
|
19718
|
+
"-p",
|
|
19719
|
+
"Type",
|
|
19720
|
+
"-p",
|
|
19721
|
+
"Active",
|
|
19722
|
+
"-p",
|
|
19723
|
+
"Class",
|
|
19724
|
+
"-p",
|
|
19725
|
+
"LockedHint"
|
|
19726
|
+
])));
|
|
19727
|
+
const screens = graphical(shown.filter((s) => s !== undefined));
|
|
19728
|
+
if (screens.length === 0)
|
|
19729
|
+
return;
|
|
19730
|
+
const idle = await run("gdbus", [
|
|
19731
|
+
"call",
|
|
19732
|
+
"--session",
|
|
19733
|
+
"--dest",
|
|
19734
|
+
"org.gnome.Mutter.IdleMonitor",
|
|
19735
|
+
"--object-path",
|
|
19736
|
+
"/org/gnome/Mutter/IdleMonitor/Core",
|
|
19737
|
+
"--method",
|
|
19738
|
+
"org.gnome.Mutter.IdleMonitor.GetIdletime"
|
|
19739
|
+
]) ?? await run("xprintidle", []);
|
|
19740
|
+
const idleMs = idle === undefined ? undefined : linuxIdleMs(idle);
|
|
19741
|
+
if (idleMs === undefined)
|
|
19742
|
+
return;
|
|
19743
|
+
return { locked: screens.every((s) => s.locked), idleMs };
|
|
19744
|
+
}
|
|
19745
|
+
var WINDOWS_SCRIPT = `
|
|
19746
|
+
Add-Type @'
|
|
19747
|
+
using System; using System.Runtime.InteropServices;
|
|
19748
|
+
public static class Idle {
|
|
19749
|
+
[StructLayout(LayoutKind.Sequential)] struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }
|
|
19750
|
+
[DllImport("user32.dll")] static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);
|
|
19751
|
+
public static long Ms() {
|
|
19752
|
+
var i = new LASTINPUTINFO(); i.cbSize = (uint)Marshal.SizeOf(i);
|
|
19753
|
+
if (!GetLastInputInfo(ref i)) return -1;
|
|
19754
|
+
return (long)unchecked((uint)Environment.TickCount - i.dwTime);
|
|
19755
|
+
}
|
|
19756
|
+
}
|
|
19757
|
+
'@
|
|
19758
|
+
$session = (Get-Process -Id $PID).SessionId
|
|
19759
|
+
while ($null -ne [Console]::In.ReadLine()) {
|
|
19760
|
+
$locked = @(Get-Process LogonUI -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $session }).Count -gt 0
|
|
19761
|
+
[Console]::Out.WriteLine("$([Idle]::Ms()) $locked")
|
|
19762
|
+
}
|
|
19763
|
+
`;
|
|
19764
|
+
function windowsScreen(line) {
|
|
19765
|
+
const m = /^(-?\d+) (True|False)$/.exec(line.trim());
|
|
19766
|
+
if (!m || Number(m[1]) < 0)
|
|
19767
|
+
return;
|
|
19768
|
+
return { idleMs: Number(m[1]), locked: m[2] === "True" };
|
|
19769
|
+
}
|
|
19770
|
+
|
|
19771
|
+
class WindowsReader {
|
|
19772
|
+
child;
|
|
19773
|
+
pending = [];
|
|
19774
|
+
read() {
|
|
19775
|
+
const child = this.start();
|
|
19776
|
+
return new Promise((resolve) => {
|
|
19777
|
+
const timer = setTimeout(() => done(undefined), TIMEOUT_MS);
|
|
19778
|
+
const done = (line) => {
|
|
19779
|
+
clearTimeout(timer);
|
|
19780
|
+
resolve(line === undefined ? undefined : windowsScreen(line));
|
|
19781
|
+
};
|
|
19782
|
+
this.pending.push(done);
|
|
19783
|
+
child.stdin?.write(`
|
|
19784
|
+
`);
|
|
19785
|
+
});
|
|
19786
|
+
}
|
|
19787
|
+
start() {
|
|
19788
|
+
if (this.child && this.child.exitCode === null)
|
|
19789
|
+
return this.child;
|
|
19790
|
+
const child = spawn4("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "-"], { stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
|
|
19791
|
+
child.stdin?.write(`${WINDOWS_SCRIPT}
|
|
19792
|
+
`);
|
|
19793
|
+
createInterface2({ input: child.stdout }).on("line", (line) => this.pending.shift()?.(line));
|
|
19794
|
+
const gone = () => {
|
|
19795
|
+
if (this.child === child)
|
|
19796
|
+
this.child = undefined;
|
|
19797
|
+
for (const p of this.pending.splice(0))
|
|
19798
|
+
p(undefined);
|
|
19799
|
+
};
|
|
19800
|
+
child.on("exit", gone);
|
|
19801
|
+
child.on("error", gone);
|
|
19802
|
+
this.child = child;
|
|
19803
|
+
return child;
|
|
19804
|
+
}
|
|
19805
|
+
stop() {
|
|
19806
|
+
this.child?.kill();
|
|
19807
|
+
this.child = undefined;
|
|
19808
|
+
}
|
|
19809
|
+
}
|
|
19810
|
+
function screenReader(platform = process.platform, run = exec) {
|
|
19811
|
+
if (platform === "darwin")
|
|
19812
|
+
return { read: () => readMac(run), stop: () => {} };
|
|
19813
|
+
if (platform === "linux")
|
|
19814
|
+
return { read: () => readLinux(run), stop: () => {} };
|
|
19815
|
+
if (platform === "win32") {
|
|
19816
|
+
const reader = new WindowsReader;
|
|
19817
|
+
return { read: () => reader.read(), stop: () => reader.stop() };
|
|
19818
|
+
}
|
|
19819
|
+
return { read: async () => {
|
|
19820
|
+
return;
|
|
19821
|
+
}, stop: () => {} };
|
|
19822
|
+
}
|
|
19823
|
+
|
|
19824
|
+
// src/agent/presence.ts
|
|
19825
|
+
var READ_MS = 1e4;
|
|
19826
|
+
function presenceEnabled(hub) {
|
|
19827
|
+
return hub.ctx.store.agentConfig().presence?.enabled === true;
|
|
19828
|
+
}
|
|
19829
|
+
|
|
19830
|
+
class Presence2 {
|
|
19831
|
+
hub;
|
|
19832
|
+
reader;
|
|
19833
|
+
sent;
|
|
19834
|
+
lastError;
|
|
19835
|
+
constructor(hub, reader = screenReader()) {
|
|
19836
|
+
this.hub = hub;
|
|
19837
|
+
this.reader = reader;
|
|
19838
|
+
}
|
|
19839
|
+
status(into) {
|
|
19840
|
+
into.presence = {
|
|
19841
|
+
enabled: presenceEnabled(this.hub),
|
|
19842
|
+
present: this.sent?.present === true,
|
|
19843
|
+
...this.lastError ? { lastError: this.lastError } : {}
|
|
19844
|
+
};
|
|
19845
|
+
}
|
|
19846
|
+
async send(present, signal) {
|
|
19847
|
+
const ctx = this.hub.ctx;
|
|
19848
|
+
if (!ctx.store.machine())
|
|
19849
|
+
return;
|
|
19850
|
+
try {
|
|
19851
|
+
await session(ctx).api.presence(present, signal ?? AbortSignal.timeout(1e4));
|
|
19852
|
+
this.sent = { present, at: Date.now() };
|
|
19853
|
+
this.lastError = undefined;
|
|
19854
|
+
} catch (e) {
|
|
19855
|
+
this.lastError = e.message;
|
|
19856
|
+
}
|
|
19857
|
+
}
|
|
19858
|
+
async tick(now = Date.now()) {
|
|
19859
|
+
const screen = presenceEnabled(this.hub) ? await this.reader.read() : undefined;
|
|
19860
|
+
const present = screen !== undefined && isPresent(screen);
|
|
19861
|
+
if (present) {
|
|
19862
|
+
if (!this.sent?.present || now - this.sent.at >= PRESENCE_BEAT_MS)
|
|
19863
|
+
await this.send(true);
|
|
19864
|
+
} else if (this.sent?.present)
|
|
19865
|
+
await this.send(false);
|
|
19866
|
+
}
|
|
19867
|
+
async run(signal) {
|
|
19868
|
+
while (!signal.aborted) {
|
|
19869
|
+
await this.tick();
|
|
19870
|
+
await pause(READ_MS, signal);
|
|
19871
|
+
}
|
|
19872
|
+
if (this.sent?.present)
|
|
19873
|
+
await this.send(false, AbortSignal.timeout(3000));
|
|
19874
|
+
this.reader.stop();
|
|
19875
|
+
}
|
|
19876
|
+
}
|
|
19877
|
+
|
|
19564
19878
|
// src/quota.ts
|
|
19565
19879
|
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
19566
19880
|
|
|
19567
19881
|
// src/codexbar.ts
|
|
19568
|
-
import { spawn as
|
|
19882
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
19569
19883
|
var RUN_TIMEOUT_MS = 120000;
|
|
19570
19884
|
function runCodexbar(bin, provider, timeoutMs = RUN_TIMEOUT_MS) {
|
|
19571
19885
|
const args = ["usage", "--format", "json", ...provider ? ["--provider", provider] : []];
|
|
19572
19886
|
return new Promise((resolve) => {
|
|
19573
|
-
|
|
19887
|
+
let start;
|
|
19888
|
+
try {
|
|
19889
|
+
start = spawnable(bin, args);
|
|
19890
|
+
} catch (e) {
|
|
19891
|
+
return resolve({ code: null, stdout: "", stderr: e.message });
|
|
19892
|
+
}
|
|
19893
|
+
const child = spawn5(start.file, start.args, {
|
|
19894
|
+
windowsVerbatimArguments: start.windowsVerbatimArguments,
|
|
19895
|
+
windowsHide: true,
|
|
19896
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
19897
|
+
});
|
|
19574
19898
|
let stdout = "";
|
|
19575
19899
|
let stderr = "";
|
|
19576
19900
|
child.stdout.on("data", (d) => {
|
|
@@ -19579,7 +19903,7 @@ function runCodexbar(bin, provider, timeoutMs = RUN_TIMEOUT_MS) {
|
|
|
19579
19903
|
child.stderr.on("data", (d) => {
|
|
19580
19904
|
stderr += d;
|
|
19581
19905
|
});
|
|
19582
|
-
const timer = setTimeout(() => child
|
|
19906
|
+
const timer = setTimeout(() => killTree(child), timeoutMs);
|
|
19583
19907
|
child.on("error", (e) => {
|
|
19584
19908
|
clearTimeout(timer);
|
|
19585
19909
|
resolve({ code: null, stdout, stderr: e.message });
|
|
@@ -19935,7 +20259,7 @@ class Quota {
|
|
|
19935
20259
|
}
|
|
19936
20260
|
|
|
19937
20261
|
// src/run.ts
|
|
19938
|
-
import { spawn as
|
|
20262
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
19939
20263
|
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
19940
20264
|
import { constants as constants2 } from "node:os";
|
|
19941
20265
|
var PROGRESS_MS = 1e4;
|
|
@@ -20039,6 +20363,8 @@ class Reporter {
|
|
|
20039
20363
|
ctx;
|
|
20040
20364
|
timing;
|
|
20041
20365
|
progress;
|
|
20366
|
+
progressSent = false;
|
|
20367
|
+
dirty = false;
|
|
20042
20368
|
exit;
|
|
20043
20369
|
lastPost = Number.NEGATIVE_INFINITY;
|
|
20044
20370
|
inflight;
|
|
@@ -20060,7 +20386,8 @@ class Reporter {
|
|
|
20060
20386
|
if (JSON.stringify(next) === JSON.stringify(this.progress))
|
|
20061
20387
|
return;
|
|
20062
20388
|
this.progress = next;
|
|
20063
|
-
this.
|
|
20389
|
+
this.dirty = true;
|
|
20390
|
+
this.schedule(this.progressGap());
|
|
20064
20391
|
}
|
|
20065
20392
|
async finish(code) {
|
|
20066
20393
|
this.exit = { code, at: iso2(this.ctx.now()) };
|
|
@@ -20074,6 +20401,9 @@ class Reporter {
|
|
|
20074
20401
|
await Promise.race([settled, new Promise((r) => t = setTimeout(r, FINAL_MS))]);
|
|
20075
20402
|
clearTimeout(t);
|
|
20076
20403
|
}
|
|
20404
|
+
progressGap() {
|
|
20405
|
+
return this.progressSent ? this.timing.progressMs : 0;
|
|
20406
|
+
}
|
|
20077
20407
|
schedule(gap) {
|
|
20078
20408
|
if (this.off || this.exit)
|
|
20079
20409
|
return;
|
|
@@ -20091,11 +20421,14 @@ class Reporter {
|
|
|
20091
20421
|
}
|
|
20092
20422
|
const input = {
|
|
20093
20423
|
...this.base,
|
|
20094
|
-
at:
|
|
20424
|
+
at: this.ctx.now().toISOString(),
|
|
20095
20425
|
...this.progress ? { progress: this.progress } : {},
|
|
20096
20426
|
...this.exit ? { exit: this.exit } : {}
|
|
20097
20427
|
};
|
|
20098
20428
|
this.lastPost = this.ctx.now().getTime();
|
|
20429
|
+
if (this.progress)
|
|
20430
|
+
this.progressSent = true;
|
|
20431
|
+
this.dirty = false;
|
|
20099
20432
|
this.inflight = this.post(input).catch((e) => {
|
|
20100
20433
|
if (e instanceof UsageError && !(e instanceof WithheldError))
|
|
20101
20434
|
this.off = true;
|
|
@@ -20108,7 +20441,7 @@ class Reporter {
|
|
|
20108
20441
|
this.queued = false;
|
|
20109
20442
|
this.send();
|
|
20110
20443
|
} else
|
|
20111
|
-
this.schedule(this.timing.heartbeatMs);
|
|
20444
|
+
this.schedule(this.dirty ? this.progressGap() : this.timing.heartbeatMs);
|
|
20112
20445
|
});
|
|
20113
20446
|
}
|
|
20114
20447
|
}
|
|
@@ -20156,7 +20489,7 @@ async function runCommand(ctx, opts) {
|
|
|
20156
20489
|
}, opts.post ?? makePoster(ctx), ctx);
|
|
20157
20490
|
const stdout = opts.stdout ?? process.stdout;
|
|
20158
20491
|
const stderr = opts.stderr ?? process.stderr;
|
|
20159
|
-
const child =
|
|
20492
|
+
const child = spawn6(start.file, start.args, {
|
|
20160
20493
|
stdio: ["inherit", "pipe", "pipe"],
|
|
20161
20494
|
windowsVerbatimArguments: start.windowsVerbatimArguments
|
|
20162
20495
|
});
|
|
@@ -20218,7 +20551,13 @@ function makeAgent(ctx, opts = {}) {
|
|
|
20218
20551
|
const quota = { providers, intervalMs, ...codexbar ? { codexbar } : {} };
|
|
20219
20552
|
const agent = new Agent2(ctx, socket, (hub) => {
|
|
20220
20553
|
const q = new Quota(hub, quota);
|
|
20221
|
-
return [
|
|
20554
|
+
return [
|
|
20555
|
+
new Decisions(hub, (why) => q.now(why)),
|
|
20556
|
+
q,
|
|
20557
|
+
new Permissions(hub),
|
|
20558
|
+
new Runs(hub),
|
|
20559
|
+
new Presence2(hub)
|
|
20560
|
+
];
|
|
20222
20561
|
});
|
|
20223
20562
|
return agent;
|
|
20224
20563
|
}
|
|
@@ -20260,6 +20599,109 @@ async function runAgent(ctx, opts) {
|
|
|
20260
20599
|
|
|
20261
20600
|
// src/hook.ts
|
|
20262
20601
|
import { basename as basename3 } from "node:path";
|
|
20602
|
+
|
|
20603
|
+
// src/ran.ts
|
|
20604
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
20605
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync9 } from "node:fs";
|
|
20606
|
+
var RUN_CHECK_MS = 500;
|
|
20607
|
+
var SHELLS = new Set(["sh", "dash", "bash", "zsh", "ash", "ksh", "fish", "busybox"]);
|
|
20608
|
+
function evalForm(command) {
|
|
20609
|
+
return `eval '${command.replaceAll("'", `'"'"'`)}'`;
|
|
20610
|
+
}
|
|
20611
|
+
function program(p) {
|
|
20612
|
+
return (p.argv[0] ?? "").split(/\s/)[0]?.split("/").pop() ?? "";
|
|
20613
|
+
}
|
|
20614
|
+
function runs(p, command) {
|
|
20615
|
+
const quoted = evalForm(command);
|
|
20616
|
+
return SHELLS.has(program(p)) && p.argv.some((a) => a.includes(quoted));
|
|
20617
|
+
}
|
|
20618
|
+
function descends(pid, root, byPid) {
|
|
20619
|
+
const seen = new Set;
|
|
20620
|
+
for (let p = byPid.get(pid);p && !seen.has(p.pid); p = byPid.get(p.ppid)) {
|
|
20621
|
+
if (p.ppid === root)
|
|
20622
|
+
return true;
|
|
20623
|
+
seen.add(p.pid);
|
|
20624
|
+
}
|
|
20625
|
+
return false;
|
|
20626
|
+
}
|
|
20627
|
+
function procLinux() {
|
|
20628
|
+
const out = [];
|
|
20629
|
+
for (const name of readdirSync3("/proc")) {
|
|
20630
|
+
const pid = Number(name);
|
|
20631
|
+
if (!Number.isInteger(pid))
|
|
20632
|
+
continue;
|
|
20633
|
+
try {
|
|
20634
|
+
const stat = readFileSync9(`/proc/${pid}/stat`, "utf8");
|
|
20635
|
+
const ppid = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
20636
|
+
const argv = readFileSync9(`/proc/${pid}/cmdline`, "utf8").split("\x00").filter(Boolean);
|
|
20637
|
+
out.push({ pid, ppid, argv });
|
|
20638
|
+
} catch {}
|
|
20639
|
+
}
|
|
20640
|
+
return out;
|
|
20641
|
+
}
|
|
20642
|
+
function procPs() {
|
|
20643
|
+
return new Promise((resolve) => execFile2("ps", ["-A", "-ww", "-o", "pid=,ppid=,args="], { maxBuffer: 16 * 1024 * 1024, timeout: 5000 }, (e, stdout) => {
|
|
20644
|
+
if (e)
|
|
20645
|
+
return resolve([]);
|
|
20646
|
+
resolve(stdout.split(`
|
|
20647
|
+
`).flatMap((line) => {
|
|
20648
|
+
const m = /^\s*(\d+)\s+(\d+)\s(.*)$/.exec(line);
|
|
20649
|
+
return m ? [{ pid: Number(m[1]), ppid: Number(m[2]), argv: [m[3]] }] : [];
|
|
20650
|
+
}));
|
|
20651
|
+
}));
|
|
20652
|
+
}
|
|
20653
|
+
async function processes(platform = process.platform) {
|
|
20654
|
+
if (platform === "linux")
|
|
20655
|
+
return procLinux();
|
|
20656
|
+
if (platform === "win32")
|
|
20657
|
+
return [];
|
|
20658
|
+
return procPs();
|
|
20659
|
+
}
|
|
20660
|
+
function agentPid(procs, from = process.ppid) {
|
|
20661
|
+
const byPid = new Map(procs.map((p) => [p.pid, p]));
|
|
20662
|
+
for (let pid = from, hops = 0;hops < 4; hops++) {
|
|
20663
|
+
const p = byPid.get(pid);
|
|
20664
|
+
if (!p)
|
|
20665
|
+
return;
|
|
20666
|
+
if (!SHELLS.has(program(p)))
|
|
20667
|
+
return pid;
|
|
20668
|
+
pid = p.ppid;
|
|
20669
|
+
}
|
|
20670
|
+
return;
|
|
20671
|
+
}
|
|
20672
|
+
function untilRan(signal, command, list = processes, from = process.ppid) {
|
|
20673
|
+
const ran = new AbortController;
|
|
20674
|
+
let timer;
|
|
20675
|
+
let stopped = false;
|
|
20676
|
+
const watch = async () => {
|
|
20677
|
+
const first = await list().catch(() => []);
|
|
20678
|
+
const root = agentPid(first, from);
|
|
20679
|
+
if (root === undefined)
|
|
20680
|
+
return;
|
|
20681
|
+
const before = new Set(first.map((p) => p.pid));
|
|
20682
|
+
const tick = async () => {
|
|
20683
|
+
if (stopped)
|
|
20684
|
+
return;
|
|
20685
|
+
const now = await list().catch(() => []);
|
|
20686
|
+
const byPid = new Map(now.map((p) => [p.pid, p]));
|
|
20687
|
+
if (now.some((p) => !before.has(p.pid) && runs(p, command) && descends(p.pid, root, byPid)))
|
|
20688
|
+
return ran.abort();
|
|
20689
|
+
timer = setTimeout(tick, RUN_CHECK_MS);
|
|
20690
|
+
timer.unref?.();
|
|
20691
|
+
};
|
|
20692
|
+
await tick();
|
|
20693
|
+
};
|
|
20694
|
+
watch();
|
|
20695
|
+
return {
|
|
20696
|
+
signal: signal ? AbortSignal.any([signal, ran.signal]) : ran.signal,
|
|
20697
|
+
stop: () => {
|
|
20698
|
+
stopped = true;
|
|
20699
|
+
clearTimeout(timer);
|
|
20700
|
+
}
|
|
20701
|
+
};
|
|
20702
|
+
}
|
|
20703
|
+
|
|
20704
|
+
// src/hook.ts
|
|
20263
20705
|
var SLACK_MS2 = 15000;
|
|
20264
20706
|
var DIRECT_POLL_SECONDS = 5;
|
|
20265
20707
|
var REPORT_MS = 5000;
|
|
@@ -20288,14 +20730,22 @@ function untilOrphaned(signal, ppid = () => process.ppid) {
|
|
|
20288
20730
|
stop: () => clearInterval(timer)
|
|
20289
20731
|
};
|
|
20290
20732
|
}
|
|
20291
|
-
async function hookPermission(outer, stdin, opts) {
|
|
20733
|
+
async function hookPermission(outer, stdin, opts, ran = untilRan) {
|
|
20292
20734
|
const watch = untilOrphaned(outer.signal);
|
|
20293
|
-
|
|
20735
|
+
let ctx = { ...outer, signal: watch.signal };
|
|
20736
|
+
let started;
|
|
20294
20737
|
try {
|
|
20738
|
+
const hook = parseHook(stdin);
|
|
20739
|
+
if (hook.tool_name === "AskUserQuestion" && opts.agent === "claude-code")
|
|
20740
|
+
return await racePicker(ctx, outer, hook, opts.wait);
|
|
20295
20741
|
if (!permissionsEnabled(ctx))
|
|
20296
20742
|
return 0;
|
|
20297
20743
|
const agent = agentName(opts.agent);
|
|
20298
|
-
const
|
|
20744
|
+
const command = hook.tool_input?.command;
|
|
20745
|
+
if (agent === "claude-code" && hook.tool_name === "Bash" && typeof command === "string") {
|
|
20746
|
+
started = ran(ctx.signal, command);
|
|
20747
|
+
ctx = { ...ctx, signal: started.signal };
|
|
20748
|
+
}
|
|
20299
20749
|
const waitMs = opts.wait ? parseDuration(opts.wait) : DEFAULT_WAIT_MS;
|
|
20300
20750
|
const deadline = ctx.now().getTime() + waitMs;
|
|
20301
20751
|
const source = permissionSource(hook, ctx.env);
|
|
@@ -20305,6 +20755,7 @@ async function hookPermission(outer, stdin, opts) {
|
|
|
20305
20755
|
} catch (e) {
|
|
20306
20756
|
ctx.err(`starbridge: permission prompt not sent: ${e.message}`);
|
|
20307
20757
|
} finally {
|
|
20758
|
+
started?.stop();
|
|
20308
20759
|
watch.stop();
|
|
20309
20760
|
}
|
|
20310
20761
|
return 0;
|
|
@@ -20406,7 +20857,11 @@ async function hookSettle(ctx, stdin, opts) {
|
|
|
20406
20857
|
const st = ctx.store.state();
|
|
20407
20858
|
if (ctx.store.promptsMarkStale(st))
|
|
20408
20859
|
ctx.store.updateState(() => {});
|
|
20409
|
-
if (!sessionId
|
|
20860
|
+
if (!sessionId)
|
|
20861
|
+
return 0;
|
|
20862
|
+
if (hook.tool_name === "AskUserQuestion" || hook.tool_input === undefined)
|
|
20863
|
+
await settlePicker(ctx, sessionId, "elsewhere");
|
|
20864
|
+
if (waitingFor(ctx.store.state(), sessionId).length === 0)
|
|
20410
20865
|
return 0;
|
|
20411
20866
|
const inputHash = hook.tool_input !== undefined ? inputHashOf(session(ctx).keys, hook.tool_input) : undefined;
|
|
20412
20867
|
if (waitingFor(ctx.store.state(), sessionId, inputHash).length === 0)
|
|
@@ -20425,41 +20880,77 @@ async function hookSettle(ctx, stdin, opts) {
|
|
|
20425
20880
|
}
|
|
20426
20881
|
return 0;
|
|
20427
20882
|
}
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
|
|
20431
|
-
|
|
20432
|
-
|
|
20433
|
-
|
|
20434
|
-
|
|
20435
|
-
|
|
20436
|
-
|
|
20437
|
-
|
|
20438
|
-
|
|
20439
|
-
|
|
20440
|
-
|
|
20883
|
+
function pickerQuestions(input) {
|
|
20884
|
+
const questions = input?.questions;
|
|
20885
|
+
if (!Array.isArray(questions) || questions.length === 0 || !questions.every((q) => typeof q?.question === "string"))
|
|
20886
|
+
return;
|
|
20887
|
+
return questions;
|
|
20888
|
+
}
|
|
20889
|
+
function pickerOpen(st, session) {
|
|
20890
|
+
return Object.entries(st.asked).filter(([id, a]) => a.picker === session && !a.settled && !st.answers[id]).map(([id]) => id);
|
|
20891
|
+
}
|
|
20892
|
+
async function settlePicker(ctx, session, outcome) {
|
|
20893
|
+
await Promise.all(pickerOpen(ctx.store.state(), session).map((id) => settle2({ ...ctx, signal: undefined }, { id, outcome }).catch((e) => ctx.err(`starbridge: could not settle ${id}: ${e.message}`))));
|
|
20894
|
+
}
|
|
20895
|
+
async function racePicker(ctx, outer, hook, waitText) {
|
|
20896
|
+
const questions = pickerQuestions(hook.tool_input);
|
|
20897
|
+
const sessionId = typeof hook.session_id === "string" ? hook.session_id : "";
|
|
20898
|
+
if (!questions || !sessionId)
|
|
20899
|
+
return 0;
|
|
20900
|
+
const s = session(ctx);
|
|
20901
|
+
const waitMs = waitText ? parseDuration(waitText) : DEFAULT_WAIT_MS;
|
|
20902
|
+
const cwd = typeof hook.cwd === "string" && hook.cwd ? hook.cwd : process.cwd();
|
|
20903
|
+
const deadline = AbortSignal.timeout(waitMs);
|
|
20904
|
+
const failed = new AbortController;
|
|
20905
|
+
const signal = AbortSignal.any([
|
|
20906
|
+
ctx.signal ?? new AbortController().signal,
|
|
20907
|
+
deadline,
|
|
20908
|
+
failed.signal
|
|
20909
|
+
]);
|
|
20910
|
+
const results = await Promise.allSettled(questions.map(async (q) => {
|
|
20911
|
+
const sub = { ...ctx, signal, err: () => {} };
|
|
20912
|
+
const input = {
|
|
20913
|
+
...questionInput({ ...q, multiple: q.multiSelect }),
|
|
20914
|
+
agent: "claude-code",
|
|
20915
|
+
session: sessionId,
|
|
20916
|
+
project: basename3(cwd),
|
|
20917
|
+
held: true,
|
|
20918
|
+
picker: sessionId,
|
|
20919
|
+
waiting: true
|
|
20920
|
+
};
|
|
20921
|
+
let id;
|
|
20922
|
+
try {
|
|
20923
|
+
({ id } = await postDecision(sub, s, resolveSource(input, ctx.env, cwd)));
|
|
20924
|
+
} catch (e) {
|
|
20925
|
+
failed.abort();
|
|
20926
|
+
throw e;
|
|
20927
|
+
}
|
|
20928
|
+
while (true) {
|
|
20929
|
+
const lines = [];
|
|
20930
|
+
const code = await wait({ ...sub, out: (l) => lines.push(l) }, { id, json: true, "no-mark": true }, s);
|
|
20931
|
+
if (code === 0 && lines[0])
|
|
20932
|
+
return opencodeAnswer(JSON.parse(lines[0]), { ...q, multiple: q.multiSelect });
|
|
20933
|
+
if (code !== EXIT_SNOOZED)
|
|
20934
|
+
return;
|
|
20935
|
+
}
|
|
20936
|
+
}));
|
|
20937
|
+
const picked = results.map((r) => r.status === "fulfilled" ? r.value : undefined);
|
|
20938
|
+
const error = results.find((r) => r.status === "rejected");
|
|
20939
|
+
if (error && !signal.aborted)
|
|
20940
|
+
ctx.err(`starbridge: question not sent: ${error.reason.message}`);
|
|
20941
|
+
if (picked.every((p) => p !== undefined) && !signal.aborted) {
|
|
20942
|
+
const answers = Object.fromEntries(questions.map((q, i) => [q.question, picked[i].join(", ")]));
|
|
20943
|
+
const decision = {
|
|
20944
|
+
behavior: "allow",
|
|
20945
|
+
updatedInput: { ...hook.tool_input, answers }
|
|
20946
|
+
};
|
|
20947
|
+
ctx.out(JSON.stringify({ hookSpecificOutput: { hookEventName: "PermissionRequest", decision } }));
|
|
20441
20948
|
return 0;
|
|
20442
20949
|
}
|
|
20443
|
-
|
|
20444
|
-
|
|
20445
|
-
|
|
20446
|
-
|
|
20447
|
-
if (Array.isArray(questions) && questions.length > 0 && questions.every((q) => typeof q?.question === "string"))
|
|
20448
|
-
input = parsed.tool_input;
|
|
20449
|
-
} catch {}
|
|
20450
|
-
const hookSpecificOutput = input ? {
|
|
20451
|
-
hookEventName: "PreToolUse",
|
|
20452
|
-
permissionDecision: "allow",
|
|
20453
|
-
updatedInput: {
|
|
20454
|
-
...input,
|
|
20455
|
-
answers: Object.fromEntries(input.questions.map((q) => [q.question, ASK_USER_REASON]))
|
|
20456
|
-
}
|
|
20457
|
-
} : {
|
|
20458
|
-
hookEventName: "PreToolUse",
|
|
20459
|
-
permissionDecision: "deny",
|
|
20460
|
-
permissionDecisionReason: ASK_USER_REASON
|
|
20461
|
-
};
|
|
20462
|
-
ctx.out(JSON.stringify({ hookSpecificOutput }));
|
|
20950
|
+
await settlePicker(ctx, sessionId, outer.signal?.aborted ? "elsewhere" : "withdrawn");
|
|
20951
|
+
return 0;
|
|
20952
|
+
}
|
|
20953
|
+
function hookAskUser() {
|
|
20463
20954
|
return 0;
|
|
20464
20955
|
}
|
|
20465
20956
|
var QUESTION_CHARS = 300;
|
|
@@ -20545,7 +21036,10 @@ async function hookQuestion(outer, stdin, opts) {
|
|
|
20545
21036
|
|
|
20546
21037
|
// src/pair.ts
|
|
20547
21038
|
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
21039
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
20548
21040
|
import { hostname } from "node:os";
|
|
21041
|
+
import { join as join8 } from "node:path";
|
|
21042
|
+
import { createInterface as createInterface3 } from "node:readline";
|
|
20549
21043
|
|
|
20550
21044
|
// ../node_modules/.pnpm/uqr@0.1.3/node_modules/uqr/dist/index.mjs
|
|
20551
21045
|
var QrCodeDataType = /* @__PURE__ */ ((QrCodeDataType2) => {
|
|
@@ -21148,7 +21642,7 @@ function terminalQr(text) {
|
|
|
21148
21642
|
|
|
21149
21643
|
// src/settings.ts
|
|
21150
21644
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
21151
|
-
import { existsSync as existsSync4, readdirSync as
|
|
21645
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4 } from "node:fs";
|
|
21152
21646
|
function detectMachineKind(env, platform = process.platform) {
|
|
21153
21647
|
if (env.CLAUDE_CODE_REMOTE === "true" || env.CODESPACES === "true" || env.GITPOD_WORKSPACE_ID)
|
|
21154
21648
|
return "cloud";
|
|
@@ -21162,7 +21656,7 @@ function hasBattery(platform) {
|
|
|
21162
21656
|
try {
|
|
21163
21657
|
if (platform === "linux") {
|
|
21164
21658
|
const dir = "/sys/class/power_supply";
|
|
21165
|
-
return existsSync4(dir) &&
|
|
21659
|
+
return existsSync4(dir) && readdirSync4(dir).some((n) => n.startsWith("BAT"));
|
|
21166
21660
|
}
|
|
21167
21661
|
if (platform === "darwin")
|
|
21168
21662
|
return spawnSync2("pmset", ["-g", "batt"], { encoding: "utf8" }).stdout.includes("InternalBattery");
|
|
@@ -21230,7 +21724,7 @@ async function offerPiAllow(ctx, prompt, quiet = false) {
|
|
|
21230
21724
|
} else
|
|
21231
21725
|
ctx.out(`Pi: pi-permission-system will ask before each \`starbridge ask\`; to allow them later, ${how}.`);
|
|
21232
21726
|
}
|
|
21233
|
-
var USAGE = "usage: starbridge config [permissions on|off] [machine-kind server|desktop|laptop|cloud]";
|
|
21727
|
+
var USAGE = "usage: starbridge config [permissions on|off] [presence on|off] [machine-kind server|desktop|laptop|cloud]";
|
|
21234
21728
|
async function configCommand(ctx, args, prompt) {
|
|
21235
21729
|
const [key, value, ...extra] = args;
|
|
21236
21730
|
if (extra.length > 0 || key !== undefined && value === undefined)
|
|
@@ -21243,6 +21737,11 @@ async function configCommand(ctx, args, prompt) {
|
|
|
21243
21737
|
await offerPiChain(ctx, prompt);
|
|
21244
21738
|
await offerPiAllow(ctx, prompt, true);
|
|
21245
21739
|
}
|
|
21740
|
+
} else if (key === "presence") {
|
|
21741
|
+
if (value !== "on" && value !== "off")
|
|
21742
|
+
throw new UsageError(USAGE);
|
|
21743
|
+
const config = ctx.store.agentConfig();
|
|
21744
|
+
ctx.store.saveAgentConfig({ ...config, presence: { enabled: value === "on" } });
|
|
21246
21745
|
} else if (key === "machine-kind") {
|
|
21247
21746
|
const kind = MachineKind.safeParse(value);
|
|
21248
21747
|
if (!kind.success)
|
|
@@ -21252,6 +21751,7 @@ async function configCommand(ctx, args, prompt) {
|
|
|
21252
21751
|
throw new UsageError(USAGE);
|
|
21253
21752
|
}
|
|
21254
21753
|
ctx.out(`permissions ${permissionsEnabled(ctx) ? "on" : "off"}`);
|
|
21754
|
+
ctx.out(`presence ${ctx.store.agentConfig().presence?.enabled ? "on" : "off"}`);
|
|
21255
21755
|
ctx.out(`machine-kind ${ctx.store.agentConfig().machineKind ?? "not set"}`);
|
|
21256
21756
|
return 0;
|
|
21257
21757
|
}
|
|
@@ -21270,6 +21770,7 @@ async function pair(ctx, opts) {
|
|
|
21270
21770
|
const id = `m_${randomBytes8(9).toString("base64url")}`;
|
|
21271
21771
|
const name = opts.name ?? previous?.name ?? hostname();
|
|
21272
21772
|
const claim = newClaimSecret();
|
|
21773
|
+
const checkKey = newCheckKey();
|
|
21273
21774
|
const deadline = ctx.now().getTime() + CODE_LIFETIME_MS;
|
|
21274
21775
|
const expired = () => new UsageError(`the pairing code expired; run \`${opts.again ?? "starbridge pair"}\` again`);
|
|
21275
21776
|
let code = newPairingCode();
|
|
@@ -21294,10 +21795,10 @@ async function pair(ctx, opts) {
|
|
|
21294
21795
|
}
|
|
21295
21796
|
const link = pairingLink(server, code);
|
|
21296
21797
|
ctx.out(`Pairing code: ${formatPairingCode(code)}`);
|
|
21297
|
-
ctx.out(" Scan this with your phone's camera
|
|
21298
|
-
for (const line of terminalQr(
|
|
21798
|
+
ctx.out(" Scan this with the Starbridge Android app or your phone's camera:");
|
|
21799
|
+
for (const line of terminalQr(appPairingLink(server, code, checkKey)))
|
|
21299
21800
|
ctx.out(line);
|
|
21300
|
-
ctx.out(`
|
|
21801
|
+
ctx.out(` No app? Open ${link}`);
|
|
21301
21802
|
ctx.out(" Or type the code under Devices in the app or web page. It expires in 10 minutes.");
|
|
21302
21803
|
let result;
|
|
21303
21804
|
while (!result) {
|
|
@@ -21325,6 +21826,20 @@ async function pair(ctx, opts) {
|
|
|
21325
21826
|
const pin = { length: approval.length, head: approval.head };
|
|
21326
21827
|
const dir = verifyDirectory(entries, { account: approval.account, pin });
|
|
21327
21828
|
checkJoined(dir, { id, role: "machine", ...publicKeys(keys) });
|
|
21829
|
+
const again = opts.again ?? "starbridge pair";
|
|
21830
|
+
const check = checkCode(entries, id);
|
|
21831
|
+
if (approval.check !== undefined) {
|
|
21832
|
+
if (!verifyCheckProof(entries, id, checkKey, approval.check))
|
|
21833
|
+
throw new UsageError(`the Android app that scanned the QR code saw another check code, so a server may have paired this machine into an account it controls. Nothing is saved: revoke "${name}" under Devices, then run \`${again}\` again`);
|
|
21834
|
+
} else {
|
|
21835
|
+
const answer = await confirmCheck(ctx, check, name);
|
|
21836
|
+
if (ctx.signal?.aborted)
|
|
21837
|
+
return 130;
|
|
21838
|
+
if (answer === "no")
|
|
21839
|
+
throw new UsageError(`the codes differ, so this machine is not paired. A server may have read the pairing code. Revoke "${name}" under Devices, then run \`${again}\` again`);
|
|
21840
|
+
if (answer === undefined)
|
|
21841
|
+
throw new UsageError(`the check code was not confirmed within 10 minutes, so this machine is not paired. Revoke "${name}" under Devices, then run \`${again}\` again`);
|
|
21842
|
+
}
|
|
21328
21843
|
const token = result.token;
|
|
21329
21844
|
ctx.store.locked(() => {
|
|
21330
21845
|
ctx.store.saveDirectory(entries);
|
|
@@ -21357,14 +21872,78 @@ async function pair(ctx, opts) {
|
|
|
21357
21872
|
function addedAt(entries, id) {
|
|
21358
21873
|
for (const e of entries) {
|
|
21359
21874
|
const body = JSON.parse(e.body);
|
|
21360
|
-
if (body.op === "add" && body.member?.id === id)
|
|
21361
|
-
|
|
21362
|
-
|
|
21363
|
-
|
|
21875
|
+
if (body.op === "add" && body.member?.id === id) {
|
|
21876
|
+
const at = new Date(body.at);
|
|
21877
|
+
const day = at.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
21878
|
+
const time = at.toLocaleTimeString(undefined, {
|
|
21364
21879
|
hour: "2-digit",
|
|
21365
21880
|
minute: "2-digit",
|
|
21366
21881
|
timeZoneName: "short"
|
|
21367
21882
|
});
|
|
21883
|
+
return `${day}, ${time}`;
|
|
21884
|
+
}
|
|
21885
|
+
}
|
|
21886
|
+
}
|
|
21887
|
+
var CONFIRM_MS = 10 * 60000;
|
|
21888
|
+
var waitingFile = (ctx) => join8(ctx.store.dir, "pair-waiting");
|
|
21889
|
+
var answerFile = (ctx, code) => join8(ctx.store.dir, `pair-answer.${code}`);
|
|
21890
|
+
function writeWhole(path, text) {
|
|
21891
|
+
const tmp = `${path}.${process.pid}`;
|
|
21892
|
+
rmSync3(tmp, { force: true });
|
|
21893
|
+
writeFileSync5(tmp, text, { mode: 384, flag: "wx" });
|
|
21894
|
+
renameSync3(tmp, path);
|
|
21895
|
+
}
|
|
21896
|
+
function sendConfirm(ctx, same) {
|
|
21897
|
+
let code;
|
|
21898
|
+
try {
|
|
21899
|
+
code = readFileSync10(waitingFile(ctx), "utf8").trim();
|
|
21900
|
+
} catch {
|
|
21901
|
+
throw new UsageError("no `starbridge pair` is waiting for its check code to be confirmed");
|
|
21902
|
+
}
|
|
21903
|
+
if (!/^[0-9A-Z]{4}(-[0-9A-Z]{4}){3}$/.test(code))
|
|
21904
|
+
throw new UsageError("no `starbridge pair` is waiting for its check code to be confirmed");
|
|
21905
|
+
writeWhole(answerFile(ctx, code), same ? "yes" : "no");
|
|
21906
|
+
ctx.out(`${same ? "Confirmed" : "Rejected"} check code ${code} for the waiting \`starbridge pair\`.`);
|
|
21907
|
+
return 0;
|
|
21908
|
+
}
|
|
21909
|
+
async function confirmCheck(ctx, code, name) {
|
|
21910
|
+
const file = answerFile(ctx, code);
|
|
21911
|
+
rmSync3(file, { force: true });
|
|
21912
|
+
mkdirSync6(ctx.store.dir, { recursive: true, mode: 448 });
|
|
21913
|
+
writeWhole(waitingFile(ctx), code);
|
|
21914
|
+
const tty = process.stdin.isTTY;
|
|
21915
|
+
ctx.out(`Check code: ${code}`);
|
|
21916
|
+
ctx.out(` The Starbridge Android app shows "${name}" under Devices with its check code. A browser shows what the server sends, so compare with the app if you have it. An app that shows no code needs updating.`);
|
|
21917
|
+
ctx.out(tty ? "Same code? [Y/n]" : " Same code? Run `starbridge pair --confirm` if so, `starbridge pair --reject` if not.");
|
|
21918
|
+
const typed = [];
|
|
21919
|
+
const shown = Date.now();
|
|
21920
|
+
const rl = tty ? createInterface3({ input: process.stdin }).on("line", (l) => {
|
|
21921
|
+
if (Date.now() - shown > 500)
|
|
21922
|
+
typed.push(l);
|
|
21923
|
+
}) : undefined;
|
|
21924
|
+
try {
|
|
21925
|
+
const until = ctx.now().getTime() + CONFIRM_MS;
|
|
21926
|
+
for (;; ) {
|
|
21927
|
+
if (ctx.signal?.aborted || ctx.now().getTime() > until)
|
|
21928
|
+
return;
|
|
21929
|
+
const line = typed.shift();
|
|
21930
|
+
if (line !== undefined)
|
|
21931
|
+
return /^\s*(y(es)?)?\s*$/i.test(line) ? "yes" : "no";
|
|
21932
|
+
try {
|
|
21933
|
+
const answer = readFileSync10(file, "utf8");
|
|
21934
|
+
rmSync3(file, { force: true });
|
|
21935
|
+
return answer === "yes" ? "yes" : "no";
|
|
21936
|
+
} catch {
|
|
21937
|
+
await ctx.sleep(500);
|
|
21938
|
+
}
|
|
21939
|
+
}
|
|
21940
|
+
} finally {
|
|
21941
|
+
rl?.close();
|
|
21942
|
+
rmSync3(file, { force: true });
|
|
21943
|
+
try {
|
|
21944
|
+
if (readFileSync10(waitingFile(ctx), "utf8") === code)
|
|
21945
|
+
rmSync3(waitingFile(ctx), { force: true });
|
|
21946
|
+
} catch {}
|
|
21368
21947
|
}
|
|
21369
21948
|
}
|
|
21370
21949
|
|
|
@@ -21484,8 +22063,8 @@ function installKind() {
|
|
|
21484
22063
|
}
|
|
21485
22064
|
|
|
21486
22065
|
// src/setup/plugins.ts
|
|
21487
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
21488
|
-
import { join as
|
|
22066
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "node:fs";
|
|
22067
|
+
import { join as join9 } from "node:path";
|
|
21489
22068
|
var MARKETPLACE = "starbridge";
|
|
21490
22069
|
var MARKETPLACE_SOURCE = "T0mSIlver/starbridge";
|
|
21491
22070
|
var PLUGINS = [`starbridge@${MARKETPLACE}`, `starbridge-mod@${MARKETPLACE}`];
|
|
@@ -21493,9 +22072,9 @@ function hasClaude(sys) {
|
|
|
21493
22072
|
return which(sys.ctx.env, "claude") !== undefined;
|
|
21494
22073
|
}
|
|
21495
22074
|
function claudeDir(sys) {
|
|
21496
|
-
return sys.ctx.env.CLAUDE_CONFIG_DIR ||
|
|
22075
|
+
return sys.ctx.env.CLAUDE_CONFIG_DIR || join9(sys.home, ".claude");
|
|
21497
22076
|
}
|
|
21498
|
-
var claude = (sys, ...args) => run(sys, "claude", args, { timeoutMs: 180000 });
|
|
22077
|
+
var claude = (sys, ...args) => run(sys, "claude", args, { timeoutMs: 180000, cwd: sys.home });
|
|
21499
22078
|
async function listJson(sys, ...args) {
|
|
21500
22079
|
const r = await claude(sys, ...args, "--json");
|
|
21501
22080
|
const why = `\`claude ${args.join(" ")} --json\` failed`;
|
|
@@ -21565,7 +22144,7 @@ async function installPlugins(sys, state) {
|
|
|
21565
22144
|
throw new Error(foreign);
|
|
21566
22145
|
const done = [];
|
|
21567
22146
|
if (!state.marketplace) {
|
|
21568
|
-
const r = await claude(sys, "plugin", "marketplace", "add", MARKETPLACE_SOURCE);
|
|
22147
|
+
const r = await claude(sys, "plugin", "marketplace", "add", MARKETPLACE_SOURCE, "--scope", "user");
|
|
21569
22148
|
if (r?.code !== 0)
|
|
21570
22149
|
throw new Error(`claude plugin marketplace add ${MARKETPLACE_SOURCE}: ${failure2(r)}`);
|
|
21571
22150
|
done.push(`Added the ${MARKETPLACE} marketplace.`);
|
|
@@ -21581,6 +22160,8 @@ async function installPlugins(sys, state) {
|
|
|
21581
22160
|
return done;
|
|
21582
22161
|
}
|
|
21583
22162
|
async function removePlugins(sys, state) {
|
|
22163
|
+
if (state.foreign)
|
|
22164
|
+
return [`Left the ${MARKETPLACE} marketplace from ${state.foreign} and its plugins alone.`];
|
|
21584
22165
|
const done = [];
|
|
21585
22166
|
for (const id of PLUGINS) {
|
|
21586
22167
|
if (!state.plugins[id])
|
|
@@ -21595,17 +22176,17 @@ async function removePlugins(sys, state) {
|
|
|
21595
22176
|
return done;
|
|
21596
22177
|
}
|
|
21597
22178
|
function settingsPath(sys) {
|
|
21598
|
-
return
|
|
22179
|
+
return join9(claudeDir(sys), "settings.json");
|
|
21599
22180
|
}
|
|
21600
22181
|
function readSettings(sys) {
|
|
21601
22182
|
try {
|
|
21602
|
-
return JSON.parse(
|
|
22183
|
+
return JSON.parse(readFileSync11(settingsPath(sys), "utf8"));
|
|
21603
22184
|
} catch {
|
|
21604
22185
|
return;
|
|
21605
22186
|
}
|
|
21606
22187
|
}
|
|
21607
22188
|
function writeSettings(sys, s) {
|
|
21608
|
-
|
|
22189
|
+
writeFileSync6(settingsPath(sys), `${JSON.stringify(s, null, 2)}
|
|
21609
22190
|
`);
|
|
21610
22191
|
}
|
|
21611
22192
|
function autoUpdate(sys) {
|
|
@@ -21646,7 +22227,7 @@ function addAllowRules(sys) {
|
|
|
21646
22227
|
...s.permissions,
|
|
21647
22228
|
allow: [...allow, ...ALLOW_RULES.filter((r) => !allow.includes(r))]
|
|
21648
22229
|
};
|
|
21649
|
-
|
|
22230
|
+
mkdirSync7(claudeDir(sys), { recursive: true });
|
|
21650
22231
|
writeSettings(sys, s);
|
|
21651
22232
|
return true;
|
|
21652
22233
|
}
|
|
@@ -21858,24 +22439,39 @@ import {
|
|
|
21858
22439
|
closeSync as closeSync2,
|
|
21859
22440
|
existsSync as existsSync6,
|
|
21860
22441
|
lstatSync as lstatSync2,
|
|
21861
|
-
mkdirSync as
|
|
22442
|
+
mkdirSync as mkdirSync8,
|
|
21862
22443
|
openSync as openSync2,
|
|
21863
|
-
readdirSync as
|
|
21864
|
-
readFileSync as
|
|
22444
|
+
readdirSync as readdirSync5,
|
|
22445
|
+
readFileSync as readFileSync12,
|
|
21865
22446
|
realpathSync as realpathSync4,
|
|
21866
|
-
renameSync as
|
|
21867
|
-
rmSync as
|
|
22447
|
+
renameSync as renameSync4,
|
|
22448
|
+
rmSync as rmSync4,
|
|
21868
22449
|
symlinkSync,
|
|
21869
22450
|
unlinkSync as unlinkSync4,
|
|
21870
22451
|
writeSync
|
|
21871
22452
|
} from "node:fs";
|
|
21872
|
-
import { dirname as dirname5, join as
|
|
22453
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
22454
|
+
// src/setup/codexbar-pin.json
|
|
22455
|
+
var codexbar_pin_default = {
|
|
22456
|
+
version: "0.73.0",
|
|
22457
|
+
sha256: {
|
|
22458
|
+
"macos-arm64": "cd8f853779f8faec003f53d5fc2a1614aef577ec9f603cbd58a9559b54e159b9",
|
|
22459
|
+
"macos-x86_64": "19e7059de688e62be98790bc10198257f5d326e4a3f4191183c23106eb91780a",
|
|
22460
|
+
"linux-aarch64": "ae6f9be046b09dc9d17210b661c25e2ee8a0150edb047b917d9eb17c295c7094",
|
|
22461
|
+
"linux-x86_64": "a84f556c7ecebc8e606e0a34ee0d121626f72d7afc1e3adf5746410bc10f9611",
|
|
22462
|
+
"linux-musl-aarch64": "a4007dfa6577a436eaeb6b46601982c69548c6e20730b3eb9133d4266d5ea92b",
|
|
22463
|
+
"linux-musl-x86_64": "75fd9cfc00784eef8badd6e76f4f74cbdf7c13db0b7f9ca6e105ddb793e6e8f8"
|
|
22464
|
+
}
|
|
22465
|
+
};
|
|
22466
|
+
|
|
22467
|
+
// src/setup/codexbar.ts
|
|
22468
|
+
var PIN = codexbar_pin_default;
|
|
21873
22469
|
var RELEASES = "https://github.com/steipete/CodexBar/releases";
|
|
21874
22470
|
var PROBE_TIMEOUT_MS = 20000;
|
|
21875
22471
|
var PROGRESS_MS2 = 5000;
|
|
21876
22472
|
var APP_HELPERS = (home) => [
|
|
21877
22473
|
"/Applications/CodexBar.app/Contents/Helpers/CodexBarCLI",
|
|
21878
|
-
|
|
22474
|
+
join10(home, "Applications/CodexBar.app/Contents/Helpers/CodexBarCLI")
|
|
21879
22475
|
];
|
|
21880
22476
|
function findCodexbar(sys, configured) {
|
|
21881
22477
|
for (const p of [configured, sys.ctx.env.STARBRIDGE_CODEXBAR])
|
|
@@ -21884,7 +22480,7 @@ function findCodexbar(sys, configured) {
|
|
|
21884
22480
|
const onPath = which(sys.ctx.env, "codexbar");
|
|
21885
22481
|
if (onPath)
|
|
21886
22482
|
return { path: onPath };
|
|
21887
|
-
const opt =
|
|
22483
|
+
const opt = join10(sys.home, ".local/opt/codexbar/codexbar");
|
|
21888
22484
|
if (existsSync6(opt))
|
|
21889
22485
|
return { path: opt };
|
|
21890
22486
|
if (sys.platform === "darwin") {
|
|
@@ -21907,7 +22503,7 @@ function tarballKey(sys, musl = isMusl()) {
|
|
|
21907
22503
|
function isMusl() {
|
|
21908
22504
|
const has = (dir, prefix) => {
|
|
21909
22505
|
try {
|
|
21910
|
-
return
|
|
22506
|
+
return readdirSync5(dir).some((f) => f.startsWith(prefix));
|
|
21911
22507
|
} catch {
|
|
21912
22508
|
return false;
|
|
21913
22509
|
}
|
|
@@ -21924,9 +22520,9 @@ function isMusl() {
|
|
|
21924
22520
|
return !libs.some((d) => has(d, "libcurl.so.4"));
|
|
21925
22521
|
}
|
|
21926
22522
|
function linkIntoLocalBin(sys, target) {
|
|
21927
|
-
const bin =
|
|
21928
|
-
const link =
|
|
21929
|
-
|
|
22523
|
+
const bin = join10(sys.home, ".local/bin");
|
|
22524
|
+
const link = join10(bin, "codexbar");
|
|
22525
|
+
mkdirSync8(bin, { recursive: true });
|
|
21930
22526
|
try {
|
|
21931
22527
|
if (!lstatSync2(link).isSymbolicLink())
|
|
21932
22528
|
return;
|
|
@@ -21935,7 +22531,7 @@ function linkIntoLocalBin(sys, target) {
|
|
|
21935
22531
|
symlinkSync(target, link);
|
|
21936
22532
|
return link;
|
|
21937
22533
|
}
|
|
21938
|
-
var optDir = (sys) =>
|
|
22534
|
+
var optDir = (sys) => join10(sys.home, ".local/opt/codexbar");
|
|
21939
22535
|
function releaseVersion(v) {
|
|
21940
22536
|
const m = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)$/.exec(v.trim());
|
|
21941
22537
|
if (!m)
|
|
@@ -21953,19 +22549,28 @@ async function fetchRelease(url, init, what) {
|
|
|
21953
22549
|
throw new Error(`${what}: GitHub is limiting requests from this address (${res.status}); try again in an hour`);
|
|
21954
22550
|
return res;
|
|
21955
22551
|
}
|
|
21956
|
-
|
|
21957
|
-
const
|
|
21958
|
-
|
|
21959
|
-
|
|
21960
|
-
|
|
21961
|
-
|
|
21962
|
-
|
|
21963
|
-
|
|
22552
|
+
function compareCodexbar(a, b) {
|
|
22553
|
+
const parse = (v) => {
|
|
22554
|
+
const [main = "", pre] = releaseVersion(v).split(/-(.*)/);
|
|
22555
|
+
return { nums: main.split(".").map(Number), pre };
|
|
22556
|
+
};
|
|
22557
|
+
const x = parse(a);
|
|
22558
|
+
const y = parse(b);
|
|
22559
|
+
for (let i = 0;i < 3; i++)
|
|
22560
|
+
if (x.nums[i] !== y.nums[i])
|
|
22561
|
+
return x.nums[i] < y.nums[i] ? -1 : 1;
|
|
22562
|
+
if (x.pre === y.pre)
|
|
22563
|
+
return 0;
|
|
22564
|
+
if (x.pre === undefined)
|
|
22565
|
+
return 1;
|
|
22566
|
+
if (y.pre === undefined)
|
|
22567
|
+
return -1;
|
|
22568
|
+
return Math.sign(x.pre.localeCompare(y.pre, "en", { numeric: true }));
|
|
21964
22569
|
}
|
|
21965
22570
|
var size = (bytes) => bytes >= 1e6 ? `${Math.round(bytes / 1e6)} MB` : `${Math.max(1, Math.round(bytes / 1000))} kB`;
|
|
21966
22571
|
function installedVersion(sys) {
|
|
21967
22572
|
try {
|
|
21968
|
-
return
|
|
22573
|
+
return readFileSync12(join10(optDir(sys), "VERSION"), "utf8").trim() || undefined;
|
|
21969
22574
|
} catch {
|
|
21970
22575
|
return;
|
|
21971
22576
|
}
|
|
@@ -21973,40 +22578,43 @@ function installedVersion(sys) {
|
|
|
21973
22578
|
|
|
21974
22579
|
class NoChecksum extends Error {
|
|
21975
22580
|
}
|
|
21976
|
-
async function installTarball(sys, key, version) {
|
|
22581
|
+
async function installTarball(sys, key, version, sha256) {
|
|
21977
22582
|
const base = `${sys.ctx.env.STARBRIDGE_CODEXBAR_RELEASES ?? RELEASES}/download`;
|
|
21978
22583
|
const name = `CodexBarCLI-v${version}-${key}.tar.gz`;
|
|
21979
22584
|
const url = `${base}/v${version}/${name}`;
|
|
21980
22585
|
const signal = AbortSignal.timeout(10 * 60000);
|
|
21981
|
-
|
|
21982
|
-
if (!
|
|
21983
|
-
|
|
21984
|
-
|
|
21985
|
-
|
|
21986
|
-
|
|
22586
|
+
let want = sha256;
|
|
22587
|
+
if (!want) {
|
|
22588
|
+
const sums = await fetchRelease(`${url}.sha256`, { signal }, `downloading ${name}.sha256`);
|
|
22589
|
+
if (!sums.ok)
|
|
22590
|
+
throw new NoChecksum(`no checksum for ${name} (${sums.status}): not installed`);
|
|
22591
|
+
want = /^[0-9a-f]{64}\b/i.exec((await sums.text()).trim())?.[0].toLowerCase();
|
|
22592
|
+
if (!want)
|
|
22593
|
+
throw new Error(`${name}.sha256 holds no SHA-256: not installed`);
|
|
22594
|
+
}
|
|
21987
22595
|
const res = await fetchRelease(url, { signal }, `downloading ${name}`);
|
|
21988
22596
|
if (!res.ok || !res.body)
|
|
21989
22597
|
throw new Error(`downloading ${name}: ${res.status}`);
|
|
21990
22598
|
const dest = optDir(sys);
|
|
21991
22599
|
const opt = dirname5(dest);
|
|
21992
|
-
const fresh =
|
|
22600
|
+
const fresh = join10(opt, `.codexbar.${process.pid}`);
|
|
21993
22601
|
const file = `${fresh}.tar.gz`;
|
|
21994
22602
|
const old = `${fresh}.old`;
|
|
21995
|
-
|
|
22603
|
+
mkdirSync8(fresh, { recursive: true });
|
|
21996
22604
|
try {
|
|
21997
22605
|
await download(sys, res, file, want, name, `CodexBar ${version} (${key})`);
|
|
21998
22606
|
const r = await run(sys, "tar", ["-xzf", file, "-C", fresh]);
|
|
21999
22607
|
if (r?.code !== 0)
|
|
22000
22608
|
throw new Error(`could not unpack ${name} into ${opt}; is the disk full? (${failure2(r)})`);
|
|
22001
22609
|
if (existsSync6(dest))
|
|
22002
|
-
|
|
22003
|
-
|
|
22610
|
+
renameSync4(dest, old);
|
|
22611
|
+
renameSync4(fresh, dest);
|
|
22004
22612
|
} finally {
|
|
22005
|
-
|
|
22006
|
-
|
|
22007
|
-
|
|
22613
|
+
rmSync4(file, { force: true });
|
|
22614
|
+
rmSync4(fresh, { recursive: true, force: true });
|
|
22615
|
+
rmSync4(old, { recursive: true, force: true });
|
|
22008
22616
|
}
|
|
22009
|
-
return
|
|
22617
|
+
return join10(dest, "codexbar");
|
|
22010
22618
|
}
|
|
22011
22619
|
async function download(sys, res, file, want, name, label) {
|
|
22012
22620
|
const total = Number(res.headers.get("content-length")) || undefined;
|
|
@@ -22035,12 +22643,15 @@ async function download(sys, res, file, want, name, label) {
|
|
|
22035
22643
|
if (sha !== want)
|
|
22036
22644
|
throw new Error(`${name} has SHA-256 ${sha}, expected ${want}: not installed`);
|
|
22037
22645
|
}
|
|
22038
|
-
async function installRelease(sys, version) {
|
|
22646
|
+
async function installRelease(sys, version, pin = PIN) {
|
|
22039
22647
|
const key = tarballKey(sys);
|
|
22040
22648
|
if (!key)
|
|
22041
22649
|
throw new Error(`no CodexBar build for ${sys.platform} ${sys.arch}`);
|
|
22042
|
-
const v = version ? releaseVersion(version) :
|
|
22043
|
-
const
|
|
22650
|
+
const v = version ? releaseVersion(version) : pin.version;
|
|
22651
|
+
const sha = v === pin.version ? pin.sha256[key] : undefined;
|
|
22652
|
+
if (v === pin.version && !sha)
|
|
22653
|
+
throw new Error(`no checksum pinned for CodexBar ${v} (${key})`);
|
|
22654
|
+
const path = await installTarball(sys, key, v, sha);
|
|
22044
22655
|
const link = linkIntoLocalBin(sys, path);
|
|
22045
22656
|
sys.ctx.out(`Installed CodexBar ${v} to ${optDir(sys)}${link ? `, linked as ${link}` : ""}.`);
|
|
22046
22657
|
return path;
|
|
@@ -22087,10 +22698,10 @@ async function listProviders(sys, bin) {
|
|
|
22087
22698
|
}
|
|
22088
22699
|
function probeSet(sys, list, named) {
|
|
22089
22700
|
const set = new Set([...list.filter((p) => p.enabled).map((p) => p.provider), ...named]);
|
|
22090
|
-
const claude = existsSync6(
|
|
22701
|
+
const claude = existsSync6(join10(sys.home, ".claude/.credentials.json")) || which(sys.ctx.env, "claude");
|
|
22091
22702
|
if (claude)
|
|
22092
22703
|
set.add("claude");
|
|
22093
|
-
if (existsSync6(
|
|
22704
|
+
if (existsSync6(join10(sys.home, ".codex/auth.json")))
|
|
22094
22705
|
set.add("codex");
|
|
22095
22706
|
return [...set];
|
|
22096
22707
|
}
|
|
@@ -22121,7 +22732,7 @@ async function probe(bin, providers, list) {
|
|
|
22121
22732
|
}
|
|
22122
22733
|
}));
|
|
22123
22734
|
}
|
|
22124
|
-
async function updateCodexbar(sys, configured, version) {
|
|
22735
|
+
async function updateCodexbar(sys, configured, version, pin = PIN) {
|
|
22125
22736
|
const out = sys.ctx.out;
|
|
22126
22737
|
const found = findCodexbar(sys, configured);
|
|
22127
22738
|
if (!found) {
|
|
@@ -22141,20 +22752,19 @@ async function updateCodexbar(sys, configured, version) {
|
|
|
22141
22752
|
return version ? 1 : 0;
|
|
22142
22753
|
}
|
|
22143
22754
|
const have = installedVersion(sys);
|
|
22144
|
-
let want;
|
|
22145
22755
|
try {
|
|
22146
|
-
want = version ? releaseVersion(version) :
|
|
22756
|
+
const want = version ? releaseVersion(version) : pin.version;
|
|
22147
22757
|
if (have === want) {
|
|
22148
22758
|
out(`CodexBar ${have} is ${version ? "installed" : "up to date"}.`);
|
|
22149
22759
|
return 0;
|
|
22150
22760
|
}
|
|
22151
|
-
|
|
22152
|
-
|
|
22153
|
-
} catch (e) {
|
|
22154
|
-
if (!version && e instanceof NoChecksum) {
|
|
22155
|
-
out(`CodexBar ${want}'s build for this machine is not published yet: kept ${have ?? "the installed one"}.`);
|
|
22761
|
+
if (!version && have && compareCodexbar(have, want) > 0) {
|
|
22762
|
+
out(`CodexBar ${have} is newer than the ${want} this release installs: kept.`);
|
|
22156
22763
|
return 0;
|
|
22157
22764
|
}
|
|
22765
|
+
await installRelease(sys, want, pin);
|
|
22766
|
+
return 0;
|
|
22767
|
+
} catch (e) {
|
|
22158
22768
|
out(`Could not update CodexBar: ${e.message}`);
|
|
22159
22769
|
return 1;
|
|
22160
22770
|
}
|
|
@@ -22165,23 +22775,23 @@ import {
|
|
|
22165
22775
|
appendFileSync as appendFileSync2,
|
|
22166
22776
|
closeSync as closeSync3,
|
|
22167
22777
|
existsSync as existsSync7,
|
|
22168
|
-
mkdirSync as
|
|
22778
|
+
mkdirSync as mkdirSync9,
|
|
22169
22779
|
openSync as openSync3,
|
|
22170
|
-
readFileSync as
|
|
22780
|
+
readFileSync as readFileSync13,
|
|
22171
22781
|
readSync as readSync2,
|
|
22172
|
-
rmSync as
|
|
22173
|
-
writeFileSync as
|
|
22782
|
+
rmSync as rmSync5,
|
|
22783
|
+
writeFileSync as writeFileSync7
|
|
22174
22784
|
} from "node:fs";
|
|
22175
|
-
import {
|
|
22785
|
+
import { delimiter, dirname as dirname6, join as join11 } from "node:path";
|
|
22176
22786
|
function recordSelf(sys) {
|
|
22177
|
-
const file =
|
|
22787
|
+
const file = join11(sys.ctx.store.dir, CLI_PATH);
|
|
22178
22788
|
if (sys.self.length !== 1 || script(sys.self[0])) {
|
|
22179
|
-
|
|
22789
|
+
rmSync5(file, { force: true });
|
|
22180
22790
|
return;
|
|
22181
22791
|
}
|
|
22182
|
-
|
|
22792
|
+
mkdirSync9(dirname6(file), { recursive: true, mode: 448 });
|
|
22183
22793
|
const self2 = sys.self[0];
|
|
22184
|
-
|
|
22794
|
+
writeFileSync7(file, `${sys.platform === "win32" ? self2.replace(/\\/g, "/") : self2}
|
|
22185
22795
|
`);
|
|
22186
22796
|
}
|
|
22187
22797
|
function script(path) {
|
|
@@ -22198,7 +22808,8 @@ function script(path) {
|
|
|
22198
22808
|
function missingDir(sys) {
|
|
22199
22809
|
if (sys.self.length !== 1)
|
|
22200
22810
|
return;
|
|
22201
|
-
const
|
|
22811
|
+
const { dirname } = pathFor(sys.platform);
|
|
22812
|
+
const dir = dirname(sys.self[0]);
|
|
22202
22813
|
const norm = (p) => {
|
|
22203
22814
|
const bare = p.replace(/[/\\]+$/, "");
|
|
22204
22815
|
return sys.platform === "win32" ? bare.toLowerCase() : bare;
|
|
@@ -22207,20 +22818,21 @@ function missingDir(sys) {
|
|
|
22207
22818
|
return path.includes(norm(dir)) ? undefined : dir;
|
|
22208
22819
|
}
|
|
22209
22820
|
function shellProfile(sys, dir) {
|
|
22210
|
-
const
|
|
22821
|
+
const { basename, join } = pathFor(sys.platform);
|
|
22822
|
+
const shell = basename(sys.ctx.env.SHELL ?? "");
|
|
22211
22823
|
const shown = dir.startsWith(`${sys.home}/`) ? `$HOME${dir.slice(sys.home.length)}` : dir;
|
|
22212
22824
|
const exported = `export PATH="${shown}:$PATH"`;
|
|
22213
22825
|
if (shell === "fish")
|
|
22214
|
-
return { file:
|
|
22826
|
+
return { file: join(sys.home, ".config", "fish", "config.fish"), line: `fish_add_path ${dir}` };
|
|
22215
22827
|
if (shell === "zsh")
|
|
22216
|
-
return { file:
|
|
22828
|
+
return { file: join(sys.ctx.env.ZDOTDIR || sys.home, ".zshrc"), line: exported };
|
|
22217
22829
|
if (shell === "bash") {
|
|
22218
22830
|
if (sys.platform !== "darwin")
|
|
22219
|
-
return { file:
|
|
22220
|
-
const login = [".bash_profile", ".bash_login"].map((f) =>
|
|
22221
|
-
return { file: login ??
|
|
22831
|
+
return { file: join(sys.home, ".bashrc"), line: exported };
|
|
22832
|
+
const login = [".bash_profile", ".bash_login"].map((f) => join(sys.home, f)).find(existsSync7);
|
|
22833
|
+
return { file: login ?? join(sys.home, ".profile"), line: exported };
|
|
22222
22834
|
}
|
|
22223
|
-
return { file:
|
|
22835
|
+
return { file: join(sys.home, ".profile"), line: exported };
|
|
22224
22836
|
}
|
|
22225
22837
|
async function pathStep(sys) {
|
|
22226
22838
|
const dir = missingDir(sys);
|
|
@@ -22236,12 +22848,13 @@ async function pathStep(sys) {
|
|
|
22236
22848
|
];
|
|
22237
22849
|
}
|
|
22238
22850
|
const { file, line } = shellProfile(sys, dir);
|
|
22851
|
+
const { dirname } = pathFor(sys.platform);
|
|
22239
22852
|
const shown = file.startsWith(`${sys.home}/`) ? `~${file.slice(sys.home.length)}` : file;
|
|
22240
|
-
const text = existsSync7(file) ?
|
|
22853
|
+
const text = existsSync7(file) ? readFileSync13(file, "utf8") : "";
|
|
22241
22854
|
const rest = dir.startsWith(`${sys.home}/`) ? dir.slice(sys.home.length) : undefined;
|
|
22242
22855
|
const has = [dir, line, ...rest ? [`$HOME${rest}`, `\${HOME}${rest}`, `~${rest}`] : []].some((spelled) => text.includes(spelled));
|
|
22243
22856
|
if (!has && await prompt.confirm(`Add ${dir} to your PATH in ${shown}?`, true)) {
|
|
22244
|
-
|
|
22857
|
+
mkdirSync9(dirname(file), { recursive: true });
|
|
22245
22858
|
appendFileSync2(file, `${text === "" || text.endsWith(`
|
|
22246
22859
|
`) ? "" : `
|
|
22247
22860
|
`}
|
|
@@ -22262,9 +22875,9 @@ ${line}
|
|
|
22262
22875
|
}
|
|
22263
22876
|
|
|
22264
22877
|
// src/setup/service.ts
|
|
22265
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
22878
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync10, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
|
|
22266
22879
|
import { userInfo as userInfo2 } from "node:os";
|
|
22267
|
-
import { delimiter as delimiter2, dirname as dirname7, join as
|
|
22880
|
+
import { delimiter as delimiter2, dirname as dirname7, join as join12 } from "node:path";
|
|
22268
22881
|
var UNIT = "starbridge-agent.service";
|
|
22269
22882
|
var LABEL = "run.starbridge.agent";
|
|
22270
22883
|
var TASK = "starbridge-agent";
|
|
@@ -22272,25 +22885,25 @@ function kind(sys) {
|
|
|
22272
22885
|
return { linux: "systemd", darwin: "launchd", win32: "task" }[sys.platform];
|
|
22273
22886
|
}
|
|
22274
22887
|
function localDir(sys) {
|
|
22275
|
-
return
|
|
22888
|
+
return join12(sys.ctx.env.LOCALAPPDATA || join12(sys.home, "AppData", "Local"), "starbridge");
|
|
22276
22889
|
}
|
|
22277
|
-
var agentLog = (sys) => kind(sys) === "task" ?
|
|
22890
|
+
var agentLog = (sys) => kind(sys) === "task" ? join12(localDir(sys), "agent.log") : join12(sys.home, "Library/Logs/starbridge-agent.log");
|
|
22278
22891
|
function unitDir(sys) {
|
|
22279
|
-
return
|
|
22892
|
+
return join12(sys.home, ".config/systemd/user");
|
|
22280
22893
|
}
|
|
22281
22894
|
function servicePath(sys) {
|
|
22282
22895
|
const k = kind(sys);
|
|
22283
22896
|
if (k === "systemd")
|
|
22284
|
-
return
|
|
22897
|
+
return join12(unitDir(sys), UNIT);
|
|
22285
22898
|
if (k === "launchd")
|
|
22286
|
-
return
|
|
22899
|
+
return join12(sys.home, "Library/LaunchAgents", `${LABEL}.plist`);
|
|
22287
22900
|
if (k === "task")
|
|
22288
|
-
return
|
|
22901
|
+
return join12(localDir(sys), `${TASK}.xml`);
|
|
22289
22902
|
return;
|
|
22290
22903
|
}
|
|
22291
22904
|
function servicePathVar(sys) {
|
|
22292
22905
|
const extra = [
|
|
22293
|
-
|
|
22906
|
+
join12(sys.home, ".local/bin"),
|
|
22294
22907
|
"/opt/homebrew/bin",
|
|
22295
22908
|
"/usr/local/bin",
|
|
22296
22909
|
"/usr/bin",
|
|
@@ -22386,12 +22999,12 @@ ${env}
|
|
|
22386
22999
|
</plist>
|
|
22387
23000
|
`;
|
|
22388
23001
|
}
|
|
22389
|
-
var winArg = (s) => /[\s"]/.test(s) ? `"${s.replace(/"/g, "\\\"")}"` : s;
|
|
23002
|
+
var winArg = (s) => /[\s"]/.test(s) ? `"${s.replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/, "$1$1")}"` : s;
|
|
22390
23003
|
function taskXml(sys) {
|
|
22391
23004
|
const env = sys.ctx.env;
|
|
22392
23005
|
const name = env.USERNAME || userInfo2().username;
|
|
22393
23006
|
const user = env.USERDOMAIN ? `${env.USERDOMAIN}\\${name}` : name;
|
|
22394
|
-
const conhost =
|
|
23007
|
+
const conhost = join12(env.SystemRoot || env.SYSTEMROOT || "C:\\Windows", "System32", "conhost.exe");
|
|
22395
23008
|
const args = ["--headless", ...sys.self, "agent", "--log", agentLog(sys)].map(winArg).join(" ");
|
|
22396
23009
|
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
22397
23010
|
${marker("<!--", "-->")}
|
|
@@ -22443,7 +23056,7 @@ ${marker("<!--", "-->")}
|
|
|
22443
23056
|
</Task>
|
|
22444
23057
|
`;
|
|
22445
23058
|
}
|
|
22446
|
-
var powershell = (sys, script, env = {}) => run(sys,
|
|
23059
|
+
var powershell = (sys, script, env = {}) => run(sys, join12(sys.ctx.env.SystemRoot || sys.ctx.env.SYSTEMROOT || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { timeoutMs: 60000, env });
|
|
22447
23060
|
async function stopTask(sys) {
|
|
22448
23061
|
let pid;
|
|
22449
23062
|
const socket = socketPath(sys.ctx.env, sys.ctx.store.dir, sys.platform);
|
|
@@ -22489,7 +23102,7 @@ async function unavailable(sys) {
|
|
|
22489
23102
|
}
|
|
22490
23103
|
function readText2(path) {
|
|
22491
23104
|
try {
|
|
22492
|
-
const bytes =
|
|
23105
|
+
const bytes = readFileSync14(path);
|
|
22493
23106
|
if (bytes[0] === 255 && bytes[1] === 254)
|
|
22494
23107
|
return bytes.toString("utf16le").slice(1);
|
|
22495
23108
|
return bytes.toString("utf8");
|
|
@@ -22507,12 +23120,12 @@ async function installService(sys, restart) {
|
|
|
22507
23120
|
const before = readText2(path);
|
|
22508
23121
|
if (before !== undefined && !ours(before))
|
|
22509
23122
|
throw new Error(`${path} was not written by setup: remove it to install the agent`);
|
|
22510
|
-
|
|
23123
|
+
mkdirSync10(dirname7(path), { recursive: true });
|
|
22511
23124
|
const k = kind(sys);
|
|
22512
23125
|
const text = k === "systemd" ? unitText(sys) : k === "launchd" ? plistText(sys) : taskXml(sys);
|
|
22513
23126
|
const changed = readText2(path) !== text;
|
|
22514
23127
|
if (changed)
|
|
22515
|
-
|
|
23128
|
+
writeFileSync8(path, k === "task" ? `\uFEFF${text}` : text, k === "task" ? "utf16le" : "utf8");
|
|
22516
23129
|
const running = /^(active|running)$/i.test((await serviceState(sys)).state);
|
|
22517
23130
|
const go = changed || restart || !running;
|
|
22518
23131
|
if (k === "task") {
|
|
@@ -22555,16 +23168,16 @@ async function removeService(sys) {
|
|
|
22555
23168
|
const r = await powershell(sys, `Unregister-ScheduledTask -TaskName ${TASK} -Confirm:$false -ErrorAction SilentlyContinue; if (Get-ScheduledTask -TaskName ${TASK} -ErrorAction SilentlyContinue) { exit 1 }`);
|
|
22556
23169
|
if (r?.code !== 0)
|
|
22557
23170
|
throw new Error(`Unregister-ScheduledTask: ${failure2(r)}`);
|
|
22558
|
-
|
|
23171
|
+
rmSync6(path, { force: true });
|
|
22559
23172
|
} else if (kind(sys) === "systemd") {
|
|
22560
23173
|
await stopUnit(sys, UNIT);
|
|
22561
|
-
|
|
23174
|
+
rmSync6(path, { force: true });
|
|
22562
23175
|
await systemctl(sys, "daemon-reload");
|
|
22563
23176
|
} else {
|
|
22564
23177
|
const r = await launchctl(sys, "bootout", `gui/${sys.uid}/${LABEL}`);
|
|
22565
23178
|
if (r?.code !== 0 && r?.code !== 3 && r?.code !== 113)
|
|
22566
23179
|
throw new Error(`launchctl bootout: ${failure2(r)}`);
|
|
22567
|
-
|
|
23180
|
+
rmSync6(path, { force: true });
|
|
22568
23181
|
}
|
|
22569
23182
|
return true;
|
|
22570
23183
|
}
|
|
@@ -22771,6 +23384,7 @@ async function setup(sys, opts) {
|
|
|
22771
23384
|
for (const line of commandTable([
|
|
22772
23385
|
["Check it", "starbridge status"],
|
|
22773
23386
|
...permissionsEnabled(ctx) ? [] : [["Send permission prompts", "starbridge config permissions on"]],
|
|
23387
|
+
...ctx.store.agentConfig().presence?.enabled || !["desktop", "laptop"].includes(ctx.store.agentConfig().machineKind ?? "") ? [] : [["Quiet the phone while here", "starbridge config presence on"]],
|
|
22774
23388
|
["Remove from one agent", `starbridge uninstall --agent ${one}`],
|
|
22775
23389
|
["Remove everything", "starbridge uninstall"]
|
|
22776
23390
|
]))
|
|
@@ -23137,6 +23751,9 @@ async function status(sys) {
|
|
|
23137
23751
|
out(`Sessions: ${agent.sessions.length}`);
|
|
23138
23752
|
for (const x of agent.sessions)
|
|
23139
23753
|
out(` ${x.id}${x.title ? ` "${x.title}"` : ""}${x.client ? ` ${x.client}` : ""}, last seen ${x.lastSeenAt}`);
|
|
23754
|
+
const own = ctx.env.CLAUDECODE === "1" ? ctx.env.CLAUDE_CODE_SESSION_ID : undefined;
|
|
23755
|
+
if (own && ctx.env.CLAUDE_CODE_SESSION_ATTENDED !== "0" && !agent.sessions.some((x) => x.id === own))
|
|
23756
|
+
out(` Not this session (${own}): no Starbridge mod runs in it, so answers come back only through \`starbridge wait\``);
|
|
23140
23757
|
}
|
|
23141
23758
|
const svc = await serviceState(sys);
|
|
23142
23759
|
out(svc.installed ? `Service: ${svc.state}${svc.enabled === undefined ? "" : svc.enabled ? ", enabled" : ", not enabled"}` : "Service: not installed");
|
|
@@ -23192,14 +23809,14 @@ async function status(sys) {
|
|
|
23192
23809
|
}
|
|
23193
23810
|
|
|
23194
23811
|
// src/setup/uninstall.ts
|
|
23195
|
-
import { existsSync as existsSync10, realpathSync as realpathSync5, rmSync as
|
|
23196
|
-
import { join as
|
|
23812
|
+
import { existsSync as existsSync10, realpathSync as realpathSync5, rmSync as rmSync8 } from "node:fs";
|
|
23813
|
+
import { join as join14 } from "node:path";
|
|
23197
23814
|
|
|
23198
23815
|
// src/update.ts
|
|
23199
|
-
import { spawn as
|
|
23200
|
-
import { chmodSync as chmodSync3, existsSync as existsSync9, readdirSync as
|
|
23816
|
+
import { spawn as spawn7, spawnSync as spawnSync3 } from "node:child_process";
|
|
23817
|
+
import { chmodSync as chmodSync3, existsSync as existsSync9, readdirSync as readdirSync6, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
23201
23818
|
import { homedir as homedir5 } from "node:os";
|
|
23202
|
-
import { basename as
|
|
23819
|
+
import { basename as basename4, dirname as dirname8, join as join13 } from "node:path";
|
|
23203
23820
|
var MANAGED = {
|
|
23204
23821
|
brew: {
|
|
23205
23822
|
update: "brew upgrade starbridge, then starbridge setup --refresh",
|
|
@@ -23235,8 +23852,8 @@ async function restartAgent(ctx) {
|
|
|
23235
23852
|
return;
|
|
23236
23853
|
}
|
|
23237
23854
|
const home = ctx.env.HOME ?? homedir5();
|
|
23238
|
-
const linux =
|
|
23239
|
-
const mac =
|
|
23855
|
+
const linux = join13(home, ".config/systemd/user/starbridge-agent.service");
|
|
23856
|
+
const mac = join13(home, "Library/LaunchAgents/run.starbridge.agent.plist");
|
|
23240
23857
|
const r = existsSync9(linux) ? sh(ctx, "systemctl", ["--user", "try-restart", "starbridge-agent.service"]) : existsSync9(mac) ? sh(ctx, "launchctl", ["kickstart", "-k", `gui/${process.getuid?.()}/run.starbridge.agent`]) : undefined;
|
|
23241
23858
|
if (r === undefined)
|
|
23242
23859
|
return;
|
|
@@ -23272,7 +23889,14 @@ async function update(ctx, install, pubkey = RELEASE_KEY, codexbar) {
|
|
|
23272
23889
|
return updateCodexbar(sys, configured, codexbar);
|
|
23273
23890
|
let self2 = 0;
|
|
23274
23891
|
try {
|
|
23275
|
-
await updateSelf(ctx, install, pubkey)
|
|
23892
|
+
if (await updateSelf(ctx, install, pubkey) && install.kind === "binary") {
|
|
23893
|
+
const r = spawnSync3(install.path, ["update"], {
|
|
23894
|
+
env: ctx.env,
|
|
23895
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
23896
|
+
timeout: 15 * 60000
|
|
23897
|
+
});
|
|
23898
|
+
return r.status ?? 1;
|
|
23899
|
+
}
|
|
23276
23900
|
} catch (e) {
|
|
23277
23901
|
if (e instanceof ReleaseError && !(e instanceof DownloadError))
|
|
23278
23902
|
throw e;
|
|
@@ -23286,17 +23910,17 @@ async function update(ctx, install, pubkey = RELEASE_KEY, codexbar) {
|
|
|
23286
23910
|
async function updateSelf(ctx, install, pubkey) {
|
|
23287
23911
|
if (install.kind !== "binary") {
|
|
23288
23912
|
ctx.out(`starbridge was installed with ${install.kind}: run ${MANAGED[install.kind].update}`);
|
|
23289
|
-
return;
|
|
23913
|
+
return false;
|
|
23290
23914
|
}
|
|
23291
23915
|
const releases = ctx.env.STARBRIDGE_RELEASES_URL ?? RELEASES_URL;
|
|
23292
23916
|
const latest = await latestVersion(releases);
|
|
23293
23917
|
if (compareVersions(latest, VERSION) <= 0) {
|
|
23294
23918
|
ctx.out(`starbridge ${VERSION} is up to date.`);
|
|
23295
|
-
return;
|
|
23919
|
+
return false;
|
|
23296
23920
|
}
|
|
23297
23921
|
const bytes = await downloadVerified(latest, platformAsset(), { releases, pubkey });
|
|
23298
|
-
const next =
|
|
23299
|
-
|
|
23922
|
+
const next = join13(dirname8(install.path), ".starbridge.new");
|
|
23923
|
+
writeFileSync9(next, bytes, { mode: 493 });
|
|
23300
23924
|
chmodSync3(next, 493);
|
|
23301
23925
|
replaceBinary(next, install.path);
|
|
23302
23926
|
ctx.out(`Updated starbridge ${VERSION} to ${latest} in ${install.path}.`);
|
|
@@ -23315,28 +23939,29 @@ async function updateSelf(ctx, install, pubkey) {
|
|
|
23315
23939
|
}
|
|
23316
23940
|
updatePlugins(ctx);
|
|
23317
23941
|
movePiPackage(ctx, latest);
|
|
23942
|
+
return true;
|
|
23318
23943
|
}
|
|
23319
23944
|
function replaceBinary(next, target, platform = process.platform) {
|
|
23320
23945
|
if (platform !== "win32")
|
|
23321
|
-
return
|
|
23946
|
+
return renameSync5(next, target);
|
|
23322
23947
|
let aside = `${target}.old`;
|
|
23323
23948
|
try {
|
|
23324
|
-
|
|
23949
|
+
rmSync7(aside, { force: true });
|
|
23325
23950
|
} catch {
|
|
23326
23951
|
aside = `${target}.${process.pid}.old`;
|
|
23327
23952
|
}
|
|
23328
|
-
|
|
23953
|
+
renameSync5(target, aside);
|
|
23329
23954
|
try {
|
|
23330
|
-
|
|
23955
|
+
renameSync5(next, target);
|
|
23331
23956
|
} catch (e) {
|
|
23332
|
-
|
|
23957
|
+
renameSync5(aside, target);
|
|
23333
23958
|
throw e;
|
|
23334
23959
|
}
|
|
23335
23960
|
const dir = dirname8(target);
|
|
23336
|
-
const leftover = (f) => f.startsWith(`${
|
|
23337
|
-
for (const f of
|
|
23961
|
+
const leftover = (f) => f.startsWith(`${basename4(target)}.`) && f.endsWith(".old");
|
|
23962
|
+
for (const f of readdirSync6(dir).filter(leftover))
|
|
23338
23963
|
try {
|
|
23339
|
-
|
|
23964
|
+
rmSync7(join13(dir, f), { force: true });
|
|
23340
23965
|
} catch {}
|
|
23341
23966
|
}
|
|
23342
23967
|
function removeBinary(ctx, install) {
|
|
@@ -23346,7 +23971,7 @@ function removeBinary(ctx, install) {
|
|
|
23346
23971
|
}
|
|
23347
23972
|
if (process.platform === "win32")
|
|
23348
23973
|
return removeRunningExe(ctx, install.path);
|
|
23349
|
-
|
|
23974
|
+
rmSync7(install.path, { force: true });
|
|
23350
23975
|
ctx.out(`Removed ${install.path}.`);
|
|
23351
23976
|
return 0;
|
|
23352
23977
|
}
|
|
@@ -23356,7 +23981,7 @@ function removeRunningExe(ctx, path) {
|
|
|
23356
23981
|
return 0;
|
|
23357
23982
|
}
|
|
23358
23983
|
const del = `ping -n 3 127.0.0.1 >nul & del /f /q "${path}" "${path}.old" "${path}.*.old"`;
|
|
23359
|
-
|
|
23984
|
+
spawn7(ctx.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", `"${del}"`], {
|
|
23360
23985
|
detached: true,
|
|
23361
23986
|
stdio: "ignore",
|
|
23362
23987
|
windowsHide: true,
|
|
@@ -23403,7 +24028,7 @@ async function uninstall(sys, opts) {
|
|
|
23403
24028
|
ctx.out(`Kept ${dir}: a service still uses it. Stop it, then rerun \`starbridge uninstall\`.`);
|
|
23404
24029
|
} else if (existsSync10(dir)) {
|
|
23405
24030
|
if (opts.purge || await prompt.confirm(`Delete ${dir} (this machine's keys and state)?`, false)) {
|
|
23406
|
-
|
|
24031
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
23407
24032
|
ctx.out(`Deleted ${dir}.`);
|
|
23408
24033
|
} else
|
|
23409
24034
|
ctx.out(`Kept ${dir}: \`starbridge setup\` reuses it.`);
|
|
@@ -23412,8 +24037,8 @@ async function uninstall(sys, opts) {
|
|
|
23412
24037
|
ctx.out(`Revoke "${machine.name}" under Devices in the Starbridge app or web page.`);
|
|
23413
24038
|
const cb = findCodexbar(sys);
|
|
23414
24039
|
if (cb) {
|
|
23415
|
-
const opt =
|
|
23416
|
-
const how = cb.inApp ? "brew uninstall --cask codexbar (or drag CodexBar.app to the Bin)" : realpathSync5(cb.path).startsWith(opt) ? `rm -rf ${opt} ${
|
|
24040
|
+
const opt = join14(sys.home, ".local/opt/codexbar");
|
|
24041
|
+
const how = cb.inApp ? "brew uninstall --cask codexbar (or drag CodexBar.app to the Bin)" : realpathSync5(cb.path).startsWith(opt) ? `rm -rf ${opt} ${join14(sys.home, ".local/bin/codexbar")}` : "your package manager";
|
|
23417
24042
|
ctx.out(`CodexBar stays installed (${cb.path}); remove it with ${how}.`);
|
|
23418
24043
|
}
|
|
23419
24044
|
if (opts.install && stopped)
|
|
@@ -23468,7 +24093,10 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
|
|
|
23468
24093
|
setup leaves it out until \`setup --agent\` brings it back.
|
|
23469
24094
|
|
|
23470
24095
|
starbridge pair [--server <url>] [--name <name>] [--force]
|
|
23471
|
-
Make this machine's keys and print a pairing code to type on a device.
|
|
24096
|
+
Make this machine's keys and print a pairing code to type on a device. Unless the
|
|
24097
|
+
Starbridge Android app scanned the QR code, confirm that its check code matches.
|
|
24098
|
+
starbridge pair --confirm | --reject
|
|
24099
|
+
Answer that for a waiting \`pair\` or \`setup\` that has no terminal.
|
|
23472
24100
|
--server <url> a self-hosted server (default: $STARBRIDGE_SERVER, else
|
|
23473
24101
|
https://starbridge.run)
|
|
23474
24102
|
|
|
@@ -23559,24 +24187,29 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
|
|
|
23559
24187
|
The commands above go through it when it runs, and to the server directly when not
|
|
23560
24188
|
(or with STARBRIDGE_NO_AGENT=1).
|
|
23561
24189
|
|
|
23562
|
-
starbridge config [permissions on|off] [machine-kind server|desktop|laptop|cloud]
|
|
24190
|
+
starbridge config [permissions on|off] [presence on|off] [machine-kind server|desktop|laptop|cloud]
|
|
23563
24191
|
Print this machine's settings, or change one. permissions: send its Claude Code
|
|
23564
24192
|
permission prompts to your devices, where they can be allowed or denied; the prompt
|
|
23565
24193
|
stays open at the keyboard and the first answer wins. Off by default; while off, the
|
|
23566
|
-
starbridge plugin's permission hook exits at once.
|
|
23567
|
-
|
|
24194
|
+
starbridge plugin's permission hook exits at once. presence: while this machine's
|
|
24195
|
+
screen is unlocked and had keyboard or mouse input in the last minute, notifications
|
|
24196
|
+
on your other devices wait a few seconds, so a question you answer here doesn't buzz
|
|
24197
|
+
your phone; the server hears only yes or no. Off by default. machine-kind: the icon
|
|
24198
|
+
devices show, detected by setup.
|
|
23568
24199
|
|
|
23569
24200
|
starbridge hook permission --agent claude-code|pi|opencode [--wait 570s]
|
|
23570
24201
|
starbridge hook settle --agent claude-code
|
|
23571
|
-
For Claude Code's PermissionRequest hook, and for its PostToolUse,
|
|
23572
|
-
Stop and SessionEnd hooks: hook JSON on stdin; prints the hook's decision, or nothing
|
|
24202
|
+
For Claude Code's PermissionRequest hook, and for its PostToolUse, PostToolUseFailure,
|
|
24203
|
+
PermissionDenied, Stop and SessionEnd hooks: hook JSON on stdin; prints the hook's decision, or nothing
|
|
23573
24204
|
to leave the prompt to the keyboard. The Starbridge Pi extension runs it with --agent pi
|
|
23574
|
-
for pi-permission-system's prompts, the opencode plugin with --agent opencode.
|
|
24205
|
+
for pi-permission-system's prompts, the opencode plugin with --agent opencode. On
|
|
24206
|
+
Claude Code's AskUserQuestion it posts each question to your devices, whatever the
|
|
24207
|
+
permissions setting, and prints the first device answers as the picker's; an answer
|
|
24208
|
+
in the picker settles them.
|
|
23575
24209
|
|
|
23576
24210
|
starbridge hook ask-user
|
|
23577
|
-
For Claude Code's PreToolUse hook on AskUserQuestion
|
|
23578
|
-
|
|
23579
|
-
through, when this machine is not paired or the server does not answer.
|
|
24211
|
+
For Claude Code's PreToolUse hook on AskUserQuestion, from older plugins: prints
|
|
24212
|
+
nothing, so the picker opens and hook permission races it.
|
|
23580
24213
|
|
|
23581
24214
|
starbridge hook question --agent opencode
|
|
23582
24215
|
For the Starbridge opencode plugin, on each call of opencode's question tool: posts each
|
|
@@ -23586,9 +24219,9 @@ var HELP = `starbridge: post decisions to your devices, report runs, upload quot
|
|
|
23586
24219
|
|
|
23587
24220
|
starbridge update [--codexbar <version>]
|
|
23588
24221
|
Install the latest release once its signature checks out, then \`setup --refresh\` (brew
|
|
23589
|
-
and npm installs: use their manager, then \`starbridge setup --refresh\`); then
|
|
23590
|
-
|
|
23591
|
-
and only that.
|
|
24222
|
+
and npm installs: use their manager, then \`starbridge setup --refresh\`); then the
|
|
24223
|
+
CodexBar release this Starbridge release pins, if setup installed CodexBar. --codexbar
|
|
24224
|
+
installs that CodexBar release instead, and only that; update keeps it if it is newer.
|
|
23592
24225
|
|
|
23593
24226
|
starbridge --version
|
|
23594
24227
|
|
|
@@ -23610,7 +24243,7 @@ function parseSessionLink(text) {
|
|
|
23610
24243
|
}
|
|
23611
24244
|
function readText3(path) {
|
|
23612
24245
|
try {
|
|
23613
|
-
return
|
|
24246
|
+
return readFileSync15(path === "-" ? 0 : path, "utf8");
|
|
23614
24247
|
} catch (e) {
|
|
23615
24248
|
throw new UsageError(`cannot read ${path === "-" ? "stdin" : path}: ${e.message}`);
|
|
23616
24249
|
}
|
|
@@ -23644,9 +24277,13 @@ async function run2(argv, ctx) {
|
|
|
23644
24277
|
options: {
|
|
23645
24278
|
server: { type: "string" },
|
|
23646
24279
|
name: { type: "string" },
|
|
23647
|
-
force: { type: "boolean" }
|
|
24280
|
+
force: { type: "boolean" },
|
|
24281
|
+
confirm: { type: "boolean" },
|
|
24282
|
+
reject: { type: "boolean" }
|
|
23648
24283
|
}
|
|
23649
24284
|
});
|
|
24285
|
+
if (values.confirm || values.reject)
|
|
24286
|
+
return sendConfirm(ctx, !values.reject);
|
|
23650
24287
|
return await pair(ctx, values);
|
|
23651
24288
|
}
|
|
23652
24289
|
case "ask": {
|
|
@@ -23892,7 +24529,7 @@ async function run2(argv, ctx) {
|
|
|
23892
24529
|
if (sub === "settle")
|
|
23893
24530
|
return await hookSettle(ctx, readText3("-"), values);
|
|
23894
24531
|
if (sub === "ask-user")
|
|
23895
|
-
return
|
|
24532
|
+
return hookAskUser();
|
|
23896
24533
|
if (sub === "question")
|
|
23897
24534
|
return await hookQuestion(ctx, readText3("-"), values);
|
|
23898
24535
|
throw new UsageError("usage: starbridge hook permission|settle --agent claude-code, starbridge hook ask-user, or starbridge hook question --agent opencode");
|