@factiii/runner 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +504 -116
- package/package.json +1 -1
- package/plugin/factiii/skills/scratchpad/SKILL.md +98 -0
package/dist/cli.js
CHANGED
|
@@ -3984,7 +3984,7 @@ var require_has_flag = __commonJS({
|
|
|
3984
3984
|
var require_supports_color = __commonJS({
|
|
3985
3985
|
"../../node_modules/debug/node_modules/supports-color/index.js"(exports2, module2) {
|
|
3986
3986
|
"use strict";
|
|
3987
|
-
var
|
|
3987
|
+
var os8 = require("os");
|
|
3988
3988
|
var hasFlag = require_has_flag();
|
|
3989
3989
|
var env = process.env;
|
|
3990
3990
|
var forceColor;
|
|
@@ -4022,7 +4022,7 @@ var require_supports_color = __commonJS({
|
|
|
4022
4022
|
}
|
|
4023
4023
|
const min = forceColor ? 1 : 0;
|
|
4024
4024
|
if (process.platform === "win32") {
|
|
4025
|
-
const osRelease =
|
|
4025
|
+
const osRelease = os8.release().split(".");
|
|
4026
4026
|
if (Number(process.versions.node.split(".")[0]) >= 8 && Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
|
|
4027
4027
|
return Number(osRelease[2]) >= 14931 ? 3 : 2;
|
|
4028
4028
|
}
|
|
@@ -12928,7 +12928,8 @@ function hostSpacePaths(spaceDir) {
|
|
|
12928
12928
|
state: import_path.default.join(spaceDir, "state"),
|
|
12929
12929
|
backups: import_path.default.join(spaceDir, "backups"),
|
|
12930
12930
|
builds: import_path.default.join(spaceDir, "builds"),
|
|
12931
|
-
bin: import_path.default.join(spaceDir, "bin")
|
|
12931
|
+
bin: import_path.default.join(spaceDir, "bin"),
|
|
12932
|
+
scratchpad: import_path.default.join(spaceDir, "scratchpad")
|
|
12932
12933
|
};
|
|
12933
12934
|
}
|
|
12934
12935
|
function worktreeFor(paths, workName) {
|
|
@@ -13885,6 +13886,7 @@ function hostTarget(spaceDir, extraEnv) {
|
|
|
13885
13886
|
const env = {};
|
|
13886
13887
|
const ports = readSpacePorts(spaceDir);
|
|
13887
13888
|
if (ports.length) env.FACTIII_PORTS = ports.join(",");
|
|
13889
|
+
env.SCRATCHPAD = paths.scratchpad;
|
|
13888
13890
|
return { ...env, ...extraEnv?.() };
|
|
13889
13891
|
},
|
|
13890
13892
|
agentEnv() {
|
|
@@ -14145,6 +14147,9 @@ function createEventSink(send, spaceSlug, notify) {
|
|
|
14145
14147
|
emitBareFsChange(postId, change) {
|
|
14146
14148
|
send(channel(`bare-fs:${postId}`), change);
|
|
14147
14149
|
},
|
|
14150
|
+
emitScratchpad(payload) {
|
|
14151
|
+
send(channel("scratchpad"), payload);
|
|
14152
|
+
},
|
|
14148
14153
|
emitActivityUpdate(activity) {
|
|
14149
14154
|
send(channel("activity-update"), activity);
|
|
14150
14155
|
}
|
|
@@ -14161,6 +14166,18 @@ var import_promises5 = __toESM(require("fs/promises"));
|
|
|
14161
14166
|
var import_os2 = __toESM(require("os"));
|
|
14162
14167
|
var import_path7 = __toESM(require("path"));
|
|
14163
14168
|
|
|
14169
|
+
// ../../shared/all/domains/products.ts
|
|
14170
|
+
var TRONS_PER_USD = 1e6;
|
|
14171
|
+
var TRONS_PER_CENT = TRONS_PER_USD / 100;
|
|
14172
|
+
var FOUNDERS_REFUND_WINDOW_DAYS = 60;
|
|
14173
|
+
var FOUNDERS_REFUND_POLICY = `Refundable within ${FOUNDERS_REFUND_WINDOW_DAYS} days of purchase \u2014 after that, all sales are final.`;
|
|
14174
|
+
|
|
14175
|
+
// ../../shared/all/domains/autoTopUp.ts
|
|
14176
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
14177
|
+
var MONTH_MS = 30 * DAY_MS;
|
|
14178
|
+
var CLAIM_STALE_MS = 5 * 60 * 1e3;
|
|
14179
|
+
var MIN_THRESHOLD_TRONS = TRONS_PER_CENT;
|
|
14180
|
+
|
|
14164
14181
|
// ../../shared/all/domains/posts.ts
|
|
14165
14182
|
var postDataSort = /* @__PURE__ */ ((postDataSort2) => {
|
|
14166
14183
|
postDataSort2["DAILY"] = "DAILY";
|
|
@@ -14363,7 +14380,11 @@ var checkPurchaseSchema = external_exports.object({
|
|
|
14363
14380
|
productId: external_exports.string(),
|
|
14364
14381
|
type: external_exports.enum(["STRIPE", "GOOGLE", "APPLE"]),
|
|
14365
14382
|
receivedPrice: external_exports.number().int().min(0, "Received price must be a positive integer in cents"),
|
|
14366
|
-
currency: external_exports.string().length(3).default("USD")
|
|
14383
|
+
currency: external_exports.string().length(3).default("USD"),
|
|
14384
|
+
// Explicit consent to keep this card for auto top-up. Card-network rules
|
|
14385
|
+
// require the buyer to agree before a card may be charged while they are not
|
|
14386
|
+
// present, so this must come from a checkbox they ticked — never a default.
|
|
14387
|
+
saveCardForAutoTopUp: external_exports.boolean().default(false)
|
|
14367
14388
|
});
|
|
14368
14389
|
var creditPurchaseSchema = external_exports.object({
|
|
14369
14390
|
orderId: external_exports.string(),
|
|
@@ -14666,6 +14687,19 @@ var skillFrontmatterSchema = external_exports.object({
|
|
|
14666
14687
|
}).strict();
|
|
14667
14688
|
var skillAgentSchema = external_exports.enum(["claude", "codex"]);
|
|
14668
14689
|
var skillScopeSchema = external_exports.enum(["global", "repo"]);
|
|
14690
|
+
var autoTopUpSchema = external_exports.object({
|
|
14691
|
+
enabled: external_exports.boolean(),
|
|
14692
|
+
productId: external_exports.number().int().positive(),
|
|
14693
|
+
thresholdTrons: external_exports.number().int().min(MIN_THRESHOLD_TRONS),
|
|
14694
|
+
dailyCapCents: external_exports.number().int().min(0),
|
|
14695
|
+
monthlyCapCents: external_exports.number().int().min(0),
|
|
14696
|
+
// Alerts. `alertBalanceTrons` is null when the low-balance warning is off;
|
|
14697
|
+
// it works independently of `enabled`, since with auto top-up off it is the
|
|
14698
|
+
// only safety net there is.
|
|
14699
|
+
alertOnTopUp: external_exports.boolean(),
|
|
14700
|
+
alertBalanceTrons: external_exports.number().int().min(MIN_THRESHOLD_TRONS).nullable(),
|
|
14701
|
+
alertNearCap: external_exports.boolean()
|
|
14702
|
+
});
|
|
14669
14703
|
|
|
14670
14704
|
// ../../shared/all/helpers/board-agent-core/host-deploy.ts
|
|
14671
14705
|
var import_child_process3 = require("child_process");
|
|
@@ -17984,6 +18018,7 @@ var TerminalModeEngine = class {
|
|
|
17984
18018
|
paths.worktrees,
|
|
17985
18019
|
paths.repo,
|
|
17986
18020
|
paths.workspace,
|
|
18021
|
+
paths.scratchpad,
|
|
17987
18022
|
`${paths.root}/drops`
|
|
17988
18023
|
];
|
|
17989
18024
|
const watcher = new WorkspaceWatcher(
|
|
@@ -19825,6 +19860,204 @@ async function getDriveRoot(accessToken) {
|
|
|
19825
19860
|
};
|
|
19826
19861
|
}
|
|
19827
19862
|
|
|
19863
|
+
// ../../shared/all/helpers/scratchpad.ts
|
|
19864
|
+
var SCRATCHPAD_MAX_BYTES = 12e6;
|
|
19865
|
+
var SCRATCHPAD_MAX_ITEMS = 200;
|
|
19866
|
+
var SCRATCHPAD_CHUNK_BYTES = 512 * 1024;
|
|
19867
|
+
var SCRATCHPAD_MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;
|
|
19868
|
+
var KIND_BY_EXT = {
|
|
19869
|
+
png: "image",
|
|
19870
|
+
jpg: "image",
|
|
19871
|
+
jpeg: "image",
|
|
19872
|
+
gif: "image",
|
|
19873
|
+
webp: "image",
|
|
19874
|
+
avif: "image",
|
|
19875
|
+
bmp: "image",
|
|
19876
|
+
svg: "image",
|
|
19877
|
+
mp4: "video",
|
|
19878
|
+
webm: "video",
|
|
19879
|
+
mov: "video",
|
|
19880
|
+
m4v: "video",
|
|
19881
|
+
mp3: "audio",
|
|
19882
|
+
wav: "audio",
|
|
19883
|
+
m4a: "audio",
|
|
19884
|
+
ogg: "audio",
|
|
19885
|
+
aac: "audio",
|
|
19886
|
+
flac: "audio",
|
|
19887
|
+
pdf: "pdf",
|
|
19888
|
+
html: "html",
|
|
19889
|
+
htm: "html",
|
|
19890
|
+
md: "markdown",
|
|
19891
|
+
markdown: "markdown",
|
|
19892
|
+
url: "link"
|
|
19893
|
+
};
|
|
19894
|
+
function scratchpadExt(name) {
|
|
19895
|
+
const dot = name.lastIndexOf(".");
|
|
19896
|
+
if (dot <= 0 || dot === name.length - 1) return "";
|
|
19897
|
+
return name.slice(dot + 1).toLowerCase();
|
|
19898
|
+
}
|
|
19899
|
+
function scratchpadKind(name) {
|
|
19900
|
+
return KIND_BY_EXT[scratchpadExt(name)] ?? "text";
|
|
19901
|
+
}
|
|
19902
|
+
function isSafeScratchpadName(name) {
|
|
19903
|
+
if (!name || name.length > 255) return false;
|
|
19904
|
+
if (name.startsWith(".")) return false;
|
|
19905
|
+
return /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(name);
|
|
19906
|
+
}
|
|
19907
|
+
|
|
19908
|
+
// ../../shared/all/helpers/board-agent-core/scratchpad-store.ts
|
|
19909
|
+
function shEscape2(s) {
|
|
19910
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19911
|
+
}
|
|
19912
|
+
var STAT_META = `stat -f '%m %z' "$f" 2>/dev/null || stat -c '%Y %s' "$f" 2>/dev/null`;
|
|
19913
|
+
var TOO_LARGE = "Scratchpad item is too large to open here.";
|
|
19914
|
+
var ScratchpadStore = class {
|
|
19915
|
+
/**
|
|
19916
|
+
* @param target Resolved per call, never held: the space's exec target is
|
|
19917
|
+
* rebuilt as the space provisions, and a captured one would go
|
|
19918
|
+
* on pointing at the tree it was built for.
|
|
19919
|
+
* @param emit Tells connected clients the directory moved. Carries a
|
|
19920
|
+
* timestamp only: the panel refetches the whole listing.
|
|
19921
|
+
*/
|
|
19922
|
+
constructor(target, emit) {
|
|
19923
|
+
this.target = target;
|
|
19924
|
+
this.emit = emit;
|
|
19925
|
+
this.watcher = null;
|
|
19926
|
+
}
|
|
19927
|
+
dir() {
|
|
19928
|
+
return this.target().paths.scratchpad;
|
|
19929
|
+
}
|
|
19930
|
+
/**
|
|
19931
|
+
* Everything on the scratchpad, newest first.
|
|
19932
|
+
*
|
|
19933
|
+
* Dotfiles are skipped: the panel is a display surface, and `.DS_Store` is
|
|
19934
|
+
* not something anyone drew. So are subdirectories: the scratchpad is flat,
|
|
19935
|
+
* which is what lets a name be an id everywhere else in this file.
|
|
19936
|
+
*/
|
|
19937
|
+
async list() {
|
|
19938
|
+
const dir = this.dir();
|
|
19939
|
+
this.startWatching();
|
|
19940
|
+
const out = await this.target().run([
|
|
19941
|
+
"sh",
|
|
19942
|
+
"-c",
|
|
19943
|
+
`mkdir -p ${shEscape2(dir)} && cd ${shEscape2(dir)} && for f in *; do
|
|
19944
|
+
[ -f "$f" ] || continue
|
|
19945
|
+
meta=$(${STAT_META}) || continue
|
|
19946
|
+
echo "$meta $f"
|
|
19947
|
+
done | LC_ALL=C sort -rn | head -n ${SCRATCHPAD_MAX_ITEMS}`
|
|
19948
|
+
]);
|
|
19949
|
+
const items = [];
|
|
19950
|
+
for (const line of out.split("\n")) {
|
|
19951
|
+
const match = /^(\d+) (\d+) (.+)$/.exec(line);
|
|
19952
|
+
if (!match) continue;
|
|
19953
|
+
const [, mtime, size, name] = match;
|
|
19954
|
+
if (!isSafeScratchpadName(name)) continue;
|
|
19955
|
+
const bytes = Number(size);
|
|
19956
|
+
items.push({
|
|
19957
|
+
name,
|
|
19958
|
+
kind: scratchpadKind(name),
|
|
19959
|
+
size: bytes,
|
|
19960
|
+
// stat reports seconds; every other timestamp on the wire is ms.
|
|
19961
|
+
modifiedAt: Number(mtime) * 1e3,
|
|
19962
|
+
tooLarge: bytes > SCRATCHPAD_MAX_BYTES
|
|
19963
|
+
});
|
|
19964
|
+
}
|
|
19965
|
+
return items;
|
|
19966
|
+
}
|
|
19967
|
+
/**
|
|
19968
|
+
* One item's bytes, base64. Capped: past SCRATCHPAD_MAX_BYTES the panel is
|
|
19969
|
+
* told to show the path instead, so a 4GB screen recording cannot pin the
|
|
19970
|
+
* data channel for minutes.
|
|
19971
|
+
*
|
|
19972
|
+
* One shell, not a size check and then a read: the panel opens every card at
|
|
19973
|
+
* once, so a second spawn per item is a doubled cost on the whole listing -
|
|
19974
|
+
* and a file that changed between the two calls would have been read past
|
|
19975
|
+
* the cap anyway.
|
|
19976
|
+
*/
|
|
19977
|
+
async read(name) {
|
|
19978
|
+
const file = shEscape2(this.resolve(name));
|
|
19979
|
+
const content = await this.target().sh(
|
|
19980
|
+
// Unquoted `$(wc -c)` on purpose: BSD wc pads its count with spaces,
|
|
19981
|
+
// which `[ -gt ]` will not parse. `base64 -w 0` is GNU-only (BSD
|
|
19982
|
+
// spells it -b), so unwrap with tr rather than asking it not to wrap.
|
|
19983
|
+
`[ -f ${file} ] || exit 1
|
|
19984
|
+
if [ $(wc -c < ${file}) -gt ${SCRATCHPAD_MAX_BYTES} ]; then
|
|
19985
|
+
echo '${TOO_LARGE}' >&2; exit 1
|
|
19986
|
+
fi
|
|
19987
|
+
base64 < ${file} | tr -d '
|
|
19988
|
+
'`
|
|
19989
|
+
).catch((err) => {
|
|
19990
|
+
const text = err instanceof Error ? err.message : "";
|
|
19991
|
+
throw new Error(
|
|
19992
|
+
text.includes(TOO_LARGE) ? TOO_LARGE : "Could not read that item."
|
|
19993
|
+
);
|
|
19994
|
+
});
|
|
19995
|
+
return { content: content.trim(), encoding: "base64" };
|
|
19996
|
+
}
|
|
19997
|
+
/**
|
|
19998
|
+
* One byte range of an item, base64. What a download is built out of.
|
|
19999
|
+
*
|
|
20000
|
+
* No size cap here, unlike `read`: the cap exists because a preview holds
|
|
20001
|
+
* the whole file in one message, and a range does not. `tail | head` rather
|
|
20002
|
+
* than `dd`, which needs a GNU-only flag to promise a full block over a pipe.
|
|
20003
|
+
*/
|
|
20004
|
+
async readChunk(name, offset, length) {
|
|
20005
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
20006
|
+
throw new Error("Invalid scratchpad offset.");
|
|
20007
|
+
}
|
|
20008
|
+
if (!Number.isInteger(length) || length <= 0 || length > SCRATCHPAD_CHUNK_BYTES) {
|
|
20009
|
+
throw new Error("Invalid scratchpad chunk length.");
|
|
20010
|
+
}
|
|
20011
|
+
const file = shEscape2(this.resolve(name));
|
|
20012
|
+
const content = await this.target().sh(
|
|
20013
|
+
// `tail -c +N` counts from 1, so the first byte is offset 0 plus one.
|
|
20014
|
+
`[ -f ${file} ] || exit 1
|
|
20015
|
+
tail -c +${offset + 1} ${file} | head -c ${length} | base64 | tr -d '
|
|
20016
|
+
'`
|
|
20017
|
+
).catch(() => {
|
|
20018
|
+
throw new Error("Could not read that item.");
|
|
20019
|
+
});
|
|
20020
|
+
return { content: content.trim(), encoding: "base64" };
|
|
20021
|
+
}
|
|
20022
|
+
async remove(name) {
|
|
20023
|
+
await this.target().run(["rm", "-f", "--", this.resolve(name)]);
|
|
20024
|
+
}
|
|
20025
|
+
/** Files only, and only at the top level: whatever put a directory in there
|
|
20026
|
+
* is not something this panel drew, and `rm -rf` on an agent-writable path
|
|
20027
|
+
* is a worse tool than this job needs. */
|
|
20028
|
+
async clear() {
|
|
20029
|
+
const dir = shEscape2(this.dir());
|
|
20030
|
+
await this.target().run([
|
|
20031
|
+
"sh",
|
|
20032
|
+
"-c",
|
|
20033
|
+
`mkdir -p ${dir} && cd ${dir} && find . -maxdepth 1 -type f -delete`
|
|
20034
|
+
]);
|
|
20035
|
+
}
|
|
20036
|
+
destroy() {
|
|
20037
|
+
const watcher = this.watcher;
|
|
20038
|
+
this.watcher = null;
|
|
20039
|
+
if (watcher) void watcher.close();
|
|
20040
|
+
}
|
|
20041
|
+
/** Idempotent: every panel that opens calls `list`, and they must not stack
|
|
20042
|
+
* watchers on one directory. */
|
|
20043
|
+
startWatching() {
|
|
20044
|
+
if (this.watcher) return;
|
|
20045
|
+
const watcher = new WorkspaceWatcher(this.dir(), () => {
|
|
20046
|
+
this.emit({ at: Date.now() });
|
|
20047
|
+
});
|
|
20048
|
+
this.watcher = watcher;
|
|
20049
|
+
watcher.start();
|
|
20050
|
+
}
|
|
20051
|
+
/** Absolute path for a name the agent chose. Flat by construction: the name
|
|
20052
|
+
* is validated, never joined with anything a caller supplied. */
|
|
20053
|
+
resolve(name) {
|
|
20054
|
+
if (!isSafeScratchpadName(name)) {
|
|
20055
|
+
throw new Error("Invalid scratchpad item name.");
|
|
20056
|
+
}
|
|
20057
|
+
return `${this.dir()}/${name}`;
|
|
20058
|
+
}
|
|
20059
|
+
};
|
|
20060
|
+
|
|
19828
20061
|
// ../../shared/all/helpers/board-agent-core/space-core.ts
|
|
19829
20062
|
var import_promises10 = require("fs/promises");
|
|
19830
20063
|
var import_path14 = __toESM(require("path"));
|
|
@@ -20340,7 +20573,7 @@ var SpaceCore = class {
|
|
|
20340
20573
|
status("Preparing workspace\u2026");
|
|
20341
20574
|
const { paths } = target;
|
|
20342
20575
|
await target.sh(
|
|
20343
|
-
`mkdir -p ${paths.root} ${paths.worktrees} ${paths.state} ${paths.backups} ${paths.builds} ${paths.bin}`
|
|
20576
|
+
`mkdir -p ${paths.root} ${paths.worktrees} ${paths.state} ${paths.backups} ${paths.builds} ${paths.bin} ${paths.scratchpad}`
|
|
20344
20577
|
);
|
|
20345
20578
|
await allocateSpacePorts(this.spaceDir());
|
|
20346
20579
|
await this.ensureSecretsBridge().catch(() => void 0);
|
|
@@ -20683,6 +20916,10 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
20683
20916
|
this.emitter = options.emitter;
|
|
20684
20917
|
this.claude = new ClaudeModeEngine(this.core, this.emitter);
|
|
20685
20918
|
this.terminal = new TerminalModeEngine(this.core, this.emitter);
|
|
20919
|
+
this.scratchpad = new ScratchpadStore(
|
|
20920
|
+
() => this.core.target(),
|
|
20921
|
+
(payload) => this.emitter.emitScratchpad(payload)
|
|
20922
|
+
);
|
|
20686
20923
|
this.gate.subscribe(() => this.projectGateOntoRun());
|
|
20687
20924
|
}
|
|
20688
20925
|
/** Everything the runner does for this space that isn't a card session, so
|
|
@@ -21659,6 +21896,22 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
21659
21896
|
bareListExposed(p) {
|
|
21660
21897
|
return this.terminal.bareListExposed(p);
|
|
21661
21898
|
}
|
|
21899
|
+
// ── Scratchpad (→ the space's display surface) ──
|
|
21900
|
+
scratchpadList() {
|
|
21901
|
+
return this.scratchpad.list();
|
|
21902
|
+
}
|
|
21903
|
+
scratchpadRead(p) {
|
|
21904
|
+
return this.scratchpad.read(p.name);
|
|
21905
|
+
}
|
|
21906
|
+
scratchpadReadChunk(p) {
|
|
21907
|
+
return this.scratchpad.readChunk(p.name, p.offset, p.length);
|
|
21908
|
+
}
|
|
21909
|
+
scratchpadDelete(p) {
|
|
21910
|
+
return this.scratchpad.remove(p.name);
|
|
21911
|
+
}
|
|
21912
|
+
scratchpadClear() {
|
|
21913
|
+
return this.scratchpad.clear();
|
|
21914
|
+
}
|
|
21662
21915
|
// ── Cross-mode coordinators ──
|
|
21663
21916
|
async cardExecute(payload) {
|
|
21664
21917
|
const {
|
|
@@ -21988,6 +22241,7 @@ ${c.content.trim() || "(no description)"}`
|
|
|
21988
22241
|
void this.codexLogin?.cancel();
|
|
21989
22242
|
this.claude.destroy();
|
|
21990
22243
|
this.terminal.destroy();
|
|
22244
|
+
this.scratchpad.destroy();
|
|
21991
22245
|
this.core.destroy();
|
|
21992
22246
|
}
|
|
21993
22247
|
/** Route one action/payload to the right engine method, for both transports.
|
|
@@ -22318,6 +22572,24 @@ ${c.content.trim() || "(no description)"}`
|
|
|
22318
22572
|
return await this.bareListExposed(
|
|
22319
22573
|
payload
|
|
22320
22574
|
);
|
|
22575
|
+
case "scratchpadList":
|
|
22576
|
+
return await this.scratchpadList();
|
|
22577
|
+
case "scratchpadRead":
|
|
22578
|
+
return await this.scratchpadRead(
|
|
22579
|
+
payload
|
|
22580
|
+
);
|
|
22581
|
+
case "scratchpadReadChunk":
|
|
22582
|
+
return await this.scratchpadReadChunk(
|
|
22583
|
+
payload
|
|
22584
|
+
);
|
|
22585
|
+
case "scratchpadDelete":
|
|
22586
|
+
await this.scratchpadDelete(
|
|
22587
|
+
payload
|
|
22588
|
+
);
|
|
22589
|
+
return void 0;
|
|
22590
|
+
case "scratchpadClear":
|
|
22591
|
+
await this.scratchpadClear();
|
|
22592
|
+
return void 0;
|
|
22321
22593
|
case "secureStatus":
|
|
22322
22594
|
return this.secureStatus();
|
|
22323
22595
|
default:
|
|
@@ -22329,11 +22601,58 @@ ${c.content.trim() || "(no description)"}`
|
|
|
22329
22601
|
// ../../shared/all/helpers/board-agent-core/index-service.ts
|
|
22330
22602
|
var import_child_process5 = require("child_process");
|
|
22331
22603
|
var import_crypto7 = require("crypto");
|
|
22332
|
-
var
|
|
22604
|
+
var import_fs6 = require("fs");
|
|
22333
22605
|
var import_promises11 = __toESM(require("fs/promises"));
|
|
22334
22606
|
var import_net3 = require("net");
|
|
22335
|
-
var
|
|
22607
|
+
var import_path17 = require("path");
|
|
22336
22608
|
var import_util7 = require("util");
|
|
22609
|
+
|
|
22610
|
+
// ../../shared/all/helpers/board-agent-core/node-version.ts
|
|
22611
|
+
var import_fs5 = require("fs");
|
|
22612
|
+
var import_os4 = __toESM(require("os"));
|
|
22613
|
+
var import_path16 = require("path");
|
|
22614
|
+
var MIN_INDEX_NODE_MAJOR = 20;
|
|
22615
|
+
var MAX_INDEX_NODE_MAJOR = 24;
|
|
22616
|
+
var NODE_OVERRIDE_ENV = "FACTIII_INDEX_NODE";
|
|
22617
|
+
function nodeMajor(version) {
|
|
22618
|
+
return Number(version.replace(/^v/, "").split(".")[0]);
|
|
22619
|
+
}
|
|
22620
|
+
function indexNodeSupported(version = process.versions.node) {
|
|
22621
|
+
const major = nodeMajor(version);
|
|
22622
|
+
return !Number.isNaN(major) && major >= MIN_INDEX_NODE_MAJOR && major <= MAX_INDEX_NODE_MAJOR;
|
|
22623
|
+
}
|
|
22624
|
+
function candidateNodes() {
|
|
22625
|
+
const paths = [];
|
|
22626
|
+
const home = import_os4.default.homedir();
|
|
22627
|
+
const nvmRoot = (0, import_path16.join)(home, ".nvm", "versions", "node");
|
|
22628
|
+
try {
|
|
22629
|
+
const versions = (0, import_fs5.readdirSync)(nvmRoot).filter((v) => indexNodeSupported(v)).sort((a, b) => nodeMajor(b) - nodeMajor(a));
|
|
22630
|
+
for (const v of versions) paths.push((0, import_path16.join)(nvmRoot, v, "bin", "node"));
|
|
22631
|
+
} catch {
|
|
22632
|
+
}
|
|
22633
|
+
for (const prefix of ["/opt/homebrew/opt", "/usr/local/opt"]) {
|
|
22634
|
+
for (let major = MAX_INDEX_NODE_MAJOR; major >= MIN_INDEX_NODE_MAJOR; major -= 1) {
|
|
22635
|
+
paths.push((0, import_path16.join)(prefix, `node@${major}`, "bin", "node"));
|
|
22636
|
+
}
|
|
22637
|
+
}
|
|
22638
|
+
return paths;
|
|
22639
|
+
}
|
|
22640
|
+
function findIndexNode() {
|
|
22641
|
+
const override = process.env[NODE_OVERRIDE_ENV];
|
|
22642
|
+
if (override && (0, import_fs5.existsSync)(override)) return override;
|
|
22643
|
+
if (indexNodeSupported()) return process.execPath;
|
|
22644
|
+
return candidateNodes().find((p) => (0, import_fs5.existsSync)(p)) ?? null;
|
|
22645
|
+
}
|
|
22646
|
+
function unsupportedNodeMessage() {
|
|
22647
|
+
return `The OneDrive index needs Node ${MIN_INDEX_NODE_MAJOR}-${MAX_INDEX_NODE_MAJOR} (pinned 24.11.1); this host runs Node ${process.versions.node} and has no supported Node installed. It compiles better-sqlite3 from source, and that build fails on Node ${MAX_INDEX_NODE_MAJOR + 1}+.
|
|
22648
|
+
|
|
22649
|
+
Install one: nvm install 24.11.1
|
|
22650
|
+
or point the runner at an existing one: export ${NODE_OVERRIDE_ENV}=/path/to/node
|
|
22651
|
+
|
|
22652
|
+
Everything else on this runner works meanwhile \u2014 only OneDrive search is off.`;
|
|
22653
|
+
}
|
|
22654
|
+
|
|
22655
|
+
// ../../shared/all/helpers/board-agent-core/index-service.ts
|
|
22337
22656
|
var execFileAsync4 = (0, import_util7.promisify)(import_child_process5.execFile);
|
|
22338
22657
|
var IndexService = class {
|
|
22339
22658
|
constructor(opts) {
|
|
@@ -22386,27 +22705,25 @@ var IndexService = class {
|
|
|
22386
22705
|
await this.waitReady(this.port);
|
|
22387
22706
|
return;
|
|
22388
22707
|
}
|
|
22389
|
-
|
|
22708
|
+
const nodeBin = findIndexNode();
|
|
22709
|
+
if (!nodeBin) throw new Error(unsupportedNodeMessage());
|
|
22710
|
+
await this.installDeps(nodeBin);
|
|
22390
22711
|
const port = await this.stablePort();
|
|
22391
22712
|
const log = await this.openLog();
|
|
22392
|
-
this.proc = (0, import_child_process5.spawn)(
|
|
22393
|
-
|
|
22394
|
-
|
|
22395
|
-
|
|
22396
|
-
|
|
22397
|
-
|
|
22398
|
-
|
|
22399
|
-
|
|
22400
|
-
|
|
22401
|
-
|
|
22402
|
-
|
|
22403
|
-
|
|
22404
|
-
|
|
22405
|
-
|
|
22406
|
-
// Own process group so stop() reaps it; nothing else reaps orphans here.
|
|
22407
|
-
detached: true
|
|
22408
|
-
}
|
|
22409
|
-
);
|
|
22713
|
+
this.proc = (0, import_child_process5.spawn)(nodeBin, [(0, import_path17.join)(this.opts.indexDir, "index.js")], {
|
|
22714
|
+
cwd: this.opts.indexDir,
|
|
22715
|
+
env: {
|
|
22716
|
+
...process.env,
|
|
22717
|
+
NODE_ENV: "production",
|
|
22718
|
+
PORT: String(port),
|
|
22719
|
+
CONTROL_TOKEN: this.opts.controlToken,
|
|
22720
|
+
EMBED_URL: this.opts.embedUrl(),
|
|
22721
|
+
INDEX_DATA_DIR: this.opts.dataDir
|
|
22722
|
+
},
|
|
22723
|
+
stdio: log ? ["ignore", log, log] : "ignore",
|
|
22724
|
+
// Own process group so stop() reaps it; nothing else reaps orphans here.
|
|
22725
|
+
detached: true
|
|
22726
|
+
});
|
|
22410
22727
|
this.proc.on("exit", () => {
|
|
22411
22728
|
this.proc = null;
|
|
22412
22729
|
this.ready = null;
|
|
@@ -22420,31 +22737,58 @@ var IndexService = class {
|
|
|
22420
22737
|
async openLog() {
|
|
22421
22738
|
try {
|
|
22422
22739
|
await import_promises11.default.mkdir(this.opts.dataDir, { recursive: true });
|
|
22423
|
-
return (0,
|
|
22740
|
+
return (0, import_fs6.openSync)((0, import_path17.join)(this.opts.dataDir, "index.log"), "a");
|
|
22424
22741
|
} catch {
|
|
22425
22742
|
return null;
|
|
22426
22743
|
}
|
|
22427
22744
|
}
|
|
22428
|
-
/** Install the index's native deps against
|
|
22429
|
-
* platform needing node-gyp fails here rather than at query time.
|
|
22430
|
-
|
|
22745
|
+
/** Install the index's native deps against the node that will RUN it, so a
|
|
22746
|
+
* platform needing node-gyp fails here rather than at query time.
|
|
22747
|
+
*
|
|
22748
|
+
* The deps are compiled per ABI, so they are only reusable under the node
|
|
22749
|
+
* that built them. The stamp records which ABI is on disk and forces a
|
|
22750
|
+
* rebuild when the interpreter changes — otherwise a host that switches
|
|
22751
|
+
* node versions loads a binding built for the old one and dies with
|
|
22752
|
+
* "compiled against a different Node.js version". */
|
|
22753
|
+
async installDeps(nodeBin) {
|
|
22431
22754
|
const dir = this.opts.indexDir;
|
|
22432
|
-
|
|
22755
|
+
const { stdout } = await execFileAsync4(nodeBin, [
|
|
22756
|
+
"-p",
|
|
22757
|
+
"process.versions.modules"
|
|
22758
|
+
]);
|
|
22759
|
+
const abi = stdout.trim();
|
|
22760
|
+
const stamp = (0, import_path17.join)(dir, ".node-abi");
|
|
22761
|
+
if ((0, import_fs6.existsSync)((0, import_path17.join)(dir, "node_modules"))) {
|
|
22762
|
+
const built = await import_promises11.default.readFile(stamp, "utf-8").catch(() => "");
|
|
22763
|
+
if (built.trim() === abi) return;
|
|
22764
|
+
await import_promises11.default.rm((0, import_path17.join)(dir, "node_modules"), { recursive: true, force: true });
|
|
22765
|
+
}
|
|
22433
22766
|
await import_promises11.default.copyFile(
|
|
22434
|
-
(0,
|
|
22435
|
-
(0,
|
|
22767
|
+
(0, import_path17.join)(dir, "image-package.json"),
|
|
22768
|
+
(0, import_path17.join)(dir, "package.json")
|
|
22436
22769
|
);
|
|
22437
22770
|
await execFileAsync4(
|
|
22438
22771
|
"npm",
|
|
22439
22772
|
["install", "--omit=dev", "--no-audit", "--no-fund"],
|
|
22440
|
-
{
|
|
22773
|
+
{
|
|
22774
|
+
cwd: dir,
|
|
22775
|
+
// npm resolves node-gyp's target from the node running it, so the
|
|
22776
|
+
// chosen interpreter has to lead PATH or the build targets the wrong ABI.
|
|
22777
|
+
env: {
|
|
22778
|
+
...process.env,
|
|
22779
|
+
PATH: `${(0, import_path17.dirname)(nodeBin)}:${process.env.PATH ?? ""}`
|
|
22780
|
+
}
|
|
22781
|
+
}
|
|
22441
22782
|
);
|
|
22783
|
+
await import_promises11.default.writeFile(stamp, `${abi}
|
|
22784
|
+
`, "utf-8").catch(() => {
|
|
22785
|
+
});
|
|
22442
22786
|
}
|
|
22443
22787
|
/** The port to listen on, kept stable across restarts. The URL goes into the
|
|
22444
22788
|
* CLIs' persistent MCP config, so a fresh port each start would leave every
|
|
22445
22789
|
* registration pointing at a dead one. */
|
|
22446
22790
|
async stablePort() {
|
|
22447
|
-
const file = (0,
|
|
22791
|
+
const file = (0, import_path17.join)(this.opts.dataDir, ".port");
|
|
22448
22792
|
const saved = Number(await import_promises11.default.readFile(file, "utf-8").catch(() => ""));
|
|
22449
22793
|
if (Number.isInteger(saved) && saved > 0 && await isFree(saved)) {
|
|
22450
22794
|
return saved;
|
|
@@ -22619,25 +22963,25 @@ async function pairWithBrowser(serverUrl) {
|
|
|
22619
22963
|
}
|
|
22620
22964
|
|
|
22621
22965
|
// src/config.ts
|
|
22622
|
-
var
|
|
22623
|
-
var
|
|
22624
|
-
var
|
|
22966
|
+
var import_fs10 = __toESM(require("fs"));
|
|
22967
|
+
var import_os7 = __toESM(require("os"));
|
|
22968
|
+
var import_path22 = __toESM(require("path"));
|
|
22625
22969
|
|
|
22626
22970
|
// src/secureFile.ts
|
|
22627
22971
|
var import_crypto10 = __toESM(require("crypto"));
|
|
22628
|
-
var
|
|
22629
|
-
var
|
|
22972
|
+
var import_fs9 = __toESM(require("fs"));
|
|
22973
|
+
var import_path21 = __toESM(require("path"));
|
|
22630
22974
|
|
|
22631
22975
|
// src/keychain.ts
|
|
22632
22976
|
var import_child_process7 = require("child_process");
|
|
22633
22977
|
var import_crypto8 = __toESM(require("crypto"));
|
|
22634
|
-
var
|
|
22635
|
-
var
|
|
22636
|
-
var
|
|
22978
|
+
var import_fs7 = __toESM(require("fs"));
|
|
22979
|
+
var import_os5 = __toESM(require("os"));
|
|
22980
|
+
var import_path18 = __toESM(require("path"));
|
|
22637
22981
|
var SERVICE = "factiii-runner";
|
|
22638
22982
|
var ACCOUNT = "config-encryption-key";
|
|
22639
|
-
var DPAPI_KEY_FILE =
|
|
22640
|
-
|
|
22983
|
+
var DPAPI_KEY_FILE = import_path18.default.join(
|
|
22984
|
+
import_os5.default.homedir(),
|
|
22641
22985
|
".factiii-runner",
|
|
22642
22986
|
"config-key.dpapi"
|
|
22643
22987
|
);
|
|
@@ -22717,7 +23061,7 @@ function linuxWrite(key) {
|
|
|
22717
23061
|
function winRead() {
|
|
22718
23062
|
let wrapped;
|
|
22719
23063
|
try {
|
|
22720
|
-
wrapped =
|
|
23064
|
+
wrapped = import_fs7.default.readFileSync(DPAPI_KEY_FILE, "utf-8").trim();
|
|
22721
23065
|
} catch {
|
|
22722
23066
|
return { state: "missing" };
|
|
22723
23067
|
}
|
|
@@ -22742,8 +23086,8 @@ function winWrite(key) {
|
|
|
22742
23086
|
"-Command",
|
|
22743
23087
|
`ConvertTo-SecureString -String '${key.toString("base64")}' -AsPlainText -Force | ConvertFrom-SecureString`
|
|
22744
23088
|
]);
|
|
22745
|
-
|
|
22746
|
-
|
|
23089
|
+
import_fs7.default.mkdirSync(import_path18.default.dirname(DPAPI_KEY_FILE), { recursive: true });
|
|
23090
|
+
import_fs7.default.writeFileSync(DPAPI_KEY_FILE, `${out.trim()}
|
|
22747
23091
|
`, { mode: 384 });
|
|
22748
23092
|
}
|
|
22749
23093
|
function keychainBackend() {
|
|
@@ -22905,22 +23249,22 @@ function keychainStatus() {
|
|
|
22905
23249
|
|
|
22906
23250
|
// src/secureStore.ts
|
|
22907
23251
|
var import_crypto9 = __toESM(require("crypto"));
|
|
22908
|
-
var
|
|
22909
|
-
var
|
|
23252
|
+
var import_fs8 = __toESM(require("fs"));
|
|
23253
|
+
var import_path20 = __toESM(require("path"));
|
|
22910
23254
|
|
|
22911
23255
|
// src/paths.ts
|
|
22912
|
-
var
|
|
22913
|
-
var
|
|
22914
|
-
var CONFIG_DIR =
|
|
23256
|
+
var import_os6 = __toESM(require("os"));
|
|
23257
|
+
var import_path19 = __toESM(require("path"));
|
|
23258
|
+
var CONFIG_DIR = import_path19.default.join(import_os6.default.homedir(), ".factiii-runner");
|
|
22915
23259
|
function safeSlug(spaceSlug) {
|
|
22916
23260
|
return spaceSlug.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
22917
23261
|
}
|
|
22918
23262
|
function spaceDirPath(spaceSlug) {
|
|
22919
|
-
return
|
|
23263
|
+
return import_path19.default.join(CONFIG_DIR, safeSlug(spaceSlug));
|
|
22920
23264
|
}
|
|
22921
23265
|
|
|
22922
23266
|
// src/secureStore.ts
|
|
22923
|
-
var VAULT_KEY_FILE =
|
|
23267
|
+
var VAULT_KEY_FILE = import_path20.default.join(CONFIG_DIR, "vault-key.json");
|
|
22924
23268
|
var SCRYPT_N2 = 1 << 15;
|
|
22925
23269
|
var MIN_PASSWORD_LENGTH = 8;
|
|
22926
23270
|
var SecureStoreError = class extends Error {
|
|
@@ -22993,7 +23337,7 @@ function unseal(box, key) {
|
|
|
22993
23337
|
function readVaultFile() {
|
|
22994
23338
|
try {
|
|
22995
23339
|
const parsed = JSON.parse(
|
|
22996
|
-
|
|
23340
|
+
import_fs8.default.readFileSync(VAULT_KEY_FILE, "utf-8")
|
|
22997
23341
|
);
|
|
22998
23342
|
return parsed.v === 1 ? parsed : null;
|
|
22999
23343
|
} catch {
|
|
@@ -23001,11 +23345,11 @@ function readVaultFile() {
|
|
|
23001
23345
|
}
|
|
23002
23346
|
}
|
|
23003
23347
|
function writeVaultFile(file) {
|
|
23004
|
-
|
|
23348
|
+
import_fs8.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
23005
23349
|
const tmp = `${VAULT_KEY_FILE}.${process.pid}.tmp`;
|
|
23006
|
-
|
|
23350
|
+
import_fs8.default.writeFileSync(tmp, `${JSON.stringify(file, null, 2)}
|
|
23007
23351
|
`, { mode: 384 });
|
|
23008
|
-
|
|
23352
|
+
import_fs8.default.renameSync(tmp, VAULT_KEY_FILE);
|
|
23009
23353
|
}
|
|
23010
23354
|
var unlockedDek = null;
|
|
23011
23355
|
function createVault(machineKey) {
|
|
@@ -23242,15 +23586,15 @@ function parseEnvelope(raw) {
|
|
|
23242
23586
|
}
|
|
23243
23587
|
}
|
|
23244
23588
|
function atomicWrite(filePath, body) {
|
|
23245
|
-
|
|
23589
|
+
import_fs9.default.mkdirSync(import_path21.default.dirname(filePath), { recursive: true });
|
|
23246
23590
|
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
23247
|
-
|
|
23248
|
-
|
|
23591
|
+
import_fs9.default.writeFileSync(tmp, body, { mode: 384 });
|
|
23592
|
+
import_fs9.default.renameSync(tmp, filePath);
|
|
23249
23593
|
}
|
|
23250
23594
|
function readSecureBuffer(filePath) {
|
|
23251
23595
|
let raw;
|
|
23252
23596
|
try {
|
|
23253
|
-
raw =
|
|
23597
|
+
raw = import_fs9.default.readFileSync(filePath, "utf-8");
|
|
23254
23598
|
} catch {
|
|
23255
23599
|
return null;
|
|
23256
23600
|
}
|
|
@@ -23307,7 +23651,7 @@ function writeSecureJson(filePath, value2) {
|
|
|
23307
23651
|
function readBootJson(filePath) {
|
|
23308
23652
|
let raw;
|
|
23309
23653
|
try {
|
|
23310
|
-
raw =
|
|
23654
|
+
raw = import_fs9.default.readFileSync(filePath, "utf-8");
|
|
23311
23655
|
} catch {
|
|
23312
23656
|
return null;
|
|
23313
23657
|
}
|
|
@@ -23357,8 +23701,8 @@ function writeBootJson(filePath, value2) {
|
|
|
23357
23701
|
}
|
|
23358
23702
|
|
|
23359
23703
|
// src/config.ts
|
|
23360
|
-
var CONFIG_DIR2 =
|
|
23361
|
-
var CONFIG_FILE =
|
|
23704
|
+
var CONFIG_DIR2 = import_path22.default.join(import_os7.default.homedir(), ".factiii-runner");
|
|
23705
|
+
var CONFIG_FILE = import_path22.default.join(CONFIG_DIR2, "config.json");
|
|
23362
23706
|
function readRunnerConfig() {
|
|
23363
23707
|
return readBootJson(CONFIG_FILE);
|
|
23364
23708
|
}
|
|
@@ -23366,7 +23710,7 @@ function writeRunnerConfig(config) {
|
|
|
23366
23710
|
writeBootJson(CONFIG_FILE, config);
|
|
23367
23711
|
}
|
|
23368
23712
|
function configExists() {
|
|
23369
|
-
return
|
|
23713
|
+
return import_fs10.default.existsSync(CONFIG_FILE);
|
|
23370
23714
|
}
|
|
23371
23715
|
|
|
23372
23716
|
// src/promptPassword.ts
|
|
@@ -23459,7 +23803,54 @@ function currentVersion() {
|
|
|
23459
23803
|
return "0.0.0";
|
|
23460
23804
|
}
|
|
23461
23805
|
}
|
|
23806
|
+
function hostSatisfiesTarget() {
|
|
23807
|
+
let engines = "";
|
|
23808
|
+
try {
|
|
23809
|
+
const res = (0, import_node_child_process.spawnSync)(
|
|
23810
|
+
"npm",
|
|
23811
|
+
["view", `${PACKAGE_NAME}@latest`, "engines.node"],
|
|
23812
|
+
{ encoding: "utf-8", shell: process.platform === "win32" }
|
|
23813
|
+
);
|
|
23814
|
+
if (res.status !== 0) return { ok: true, reason: "" };
|
|
23815
|
+
engines = (res.stdout ?? "").trim();
|
|
23816
|
+
} catch {
|
|
23817
|
+
return { ok: true, reason: "" };
|
|
23818
|
+
}
|
|
23819
|
+
if (!engines) return { ok: true, reason: "" };
|
|
23820
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
23821
|
+
if (Number.isNaN(major)) return { ok: true, reason: "" };
|
|
23822
|
+
const min = /(?:>=|\^|~)\s*v?(\d+)/.exec(engines);
|
|
23823
|
+
if (min && major < Number(min[1])) {
|
|
23824
|
+
return {
|
|
23825
|
+
ok: false,
|
|
23826
|
+
reason: `it needs Node ${engines} and this host runs Node ${process.versions.node}`
|
|
23827
|
+
};
|
|
23828
|
+
}
|
|
23829
|
+
const lessThan = /<\s*v?(\d+)/.exec(engines);
|
|
23830
|
+
if (lessThan && major >= Number(lessThan[1])) {
|
|
23831
|
+
return {
|
|
23832
|
+
ok: false,
|
|
23833
|
+
reason: `it needs Node ${engines} and this host runs Node ${process.versions.node}`
|
|
23834
|
+
};
|
|
23835
|
+
}
|
|
23836
|
+
const atMost = /<=\s*v?(\d+)/.exec(engines);
|
|
23837
|
+
if (atMost && major > Number(atMost[1])) {
|
|
23838
|
+
return {
|
|
23839
|
+
ok: false,
|
|
23840
|
+
reason: `it needs Node ${engines} and this host runs Node ${process.versions.node}`
|
|
23841
|
+
};
|
|
23842
|
+
}
|
|
23843
|
+
return { ok: true, reason: "" };
|
|
23844
|
+
}
|
|
23462
23845
|
function installLatest() {
|
|
23846
|
+
const fit = hostSatisfiesTarget();
|
|
23847
|
+
if (!fit.ok) {
|
|
23848
|
+
console.error(
|
|
23849
|
+
`[update] refusing to install ${PACKAGE_NAME}@latest: ${fit.reason}.
|
|
23850
|
+
[update] Staying on ${currentVersion()}. Install a supported Node (nvm install 24.11.1) and restart the runner to update.`
|
|
23851
|
+
);
|
|
23852
|
+
return false;
|
|
23853
|
+
}
|
|
23463
23854
|
console.log(`[update] installing ${PACKAGE_NAME}@latest...`);
|
|
23464
23855
|
const res = (0, import_node_child_process.spawnSync)("npm", ["install", "-g", `${PACKAGE_NAME}@latest`], {
|
|
23465
23856
|
stdio: "inherit",
|
|
@@ -23480,9 +23871,9 @@ var import_node_child_process2 = require("node:child_process");
|
|
|
23480
23871
|
|
|
23481
23872
|
// src/daemon.ts
|
|
23482
23873
|
var import_crypto11 = require("crypto");
|
|
23483
|
-
var
|
|
23874
|
+
var import_fs12 = __toESM(require("fs"));
|
|
23484
23875
|
var import_node_datachannel = require("node-datachannel");
|
|
23485
|
-
var
|
|
23876
|
+
var import_path24 = __toESM(require("path"));
|
|
23486
23877
|
|
|
23487
23878
|
// ../../node_modules/engine.io-client/build/esm-debug/transports/polling-xhr.node.js
|
|
23488
23879
|
var XMLHttpRequestModule = __toESM(require_XMLHttpRequest(), 1);
|
|
@@ -26981,18 +27372,18 @@ Object.assign(lookup, {
|
|
|
26981
27372
|
});
|
|
26982
27373
|
|
|
26983
27374
|
// src/agent-adapter.ts
|
|
26984
|
-
var
|
|
26985
|
-
var
|
|
27375
|
+
var import_fs11 = __toESM(require("fs"));
|
|
27376
|
+
var import_path23 = __toESM(require("path"));
|
|
26986
27377
|
function credsDir(spaceSlug) {
|
|
26987
|
-
return
|
|
27378
|
+
return import_path23.default.join(spaceDirPath(spaceSlug), ".creds");
|
|
26988
27379
|
}
|
|
26989
27380
|
function githubPath(spaceSlug) {
|
|
26990
|
-
return
|
|
27381
|
+
return import_path23.default.join(credsDir(spaceSlug), "github.json");
|
|
26991
27382
|
}
|
|
26992
27383
|
function connectionPath(spaceSlug) {
|
|
26993
|
-
return
|
|
27384
|
+
return import_path23.default.join(credsDir(spaceSlug), "onedrive.json");
|
|
26994
27385
|
}
|
|
26995
|
-
var RUNNER_CONFIG_PATH =
|
|
27386
|
+
var RUNNER_CONFIG_PATH = import_path23.default.join(CONFIG_DIR, "runner-config.json");
|
|
26996
27387
|
function writeJson(filePath, value2) {
|
|
26997
27388
|
writeSecureJson(filePath, value2);
|
|
26998
27389
|
}
|
|
@@ -27008,14 +27399,14 @@ function fillDefaults(config) {
|
|
|
27008
27399
|
}
|
|
27009
27400
|
function configDirNames() {
|
|
27010
27401
|
try {
|
|
27011
|
-
return
|
|
27402
|
+
return import_fs11.default.readdirSync(CONFIG_DIR, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
27012
27403
|
} catch {
|
|
27013
27404
|
return [];
|
|
27014
27405
|
}
|
|
27015
27406
|
}
|
|
27016
27407
|
function provisionedSpaceSlugs() {
|
|
27017
27408
|
return configDirNames().filter(
|
|
27018
|
-
(name) =>
|
|
27409
|
+
(name) => import_fs11.default.existsSync(import_path23.default.join(CONFIG_DIR, name, "bin"))
|
|
27019
27410
|
);
|
|
27020
27411
|
}
|
|
27021
27412
|
var migrated = false;
|
|
@@ -27029,7 +27420,7 @@ function migrateToSpaceFolders() {
|
|
|
27029
27420
|
}
|
|
27030
27421
|
}
|
|
27031
27422
|
function readLegacy(filePath) {
|
|
27032
|
-
if (!
|
|
27423
|
+
if (!import_fs11.default.existsSync(filePath)) return { readable: true, value: null };
|
|
27033
27424
|
const value2 = readSecureJson(filePath);
|
|
27034
27425
|
return { readable: value2 !== null, value: value2 };
|
|
27035
27426
|
}
|
|
@@ -27047,14 +27438,14 @@ function migrateOnce() {
|
|
|
27047
27438
|
const consumed = [];
|
|
27048
27439
|
let strayFiles = [];
|
|
27049
27440
|
try {
|
|
27050
|
-
strayFiles =
|
|
27441
|
+
strayFiles = import_fs11.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".json")).filter((f) => !f.endsWith(".onedrive.json"));
|
|
27051
27442
|
} catch {
|
|
27052
27443
|
strayFiles = [];
|
|
27053
27444
|
}
|
|
27054
27445
|
for (const name of strayFiles) {
|
|
27055
27446
|
const slug = name.slice("space-".length, -".json".length);
|
|
27056
27447
|
const parsed = readLegacy(
|
|
27057
|
-
|
|
27448
|
+
import_path23.default.join(CONFIG_DIR, name)
|
|
27058
27449
|
);
|
|
27059
27450
|
if (!parsed.readable) continue;
|
|
27060
27451
|
if (parsed.value) strays[slug] = parsed.value;
|
|
@@ -27067,7 +27458,7 @@ function migrateOnce() {
|
|
|
27067
27458
|
...provisionedSpaceSlugs()
|
|
27068
27459
|
]);
|
|
27069
27460
|
for (const slug of slugs) {
|
|
27070
|
-
if (
|
|
27461
|
+
if (import_fs11.default.existsSync(githubPath(slug))) continue;
|
|
27071
27462
|
const over = boards[slug] ?? strays[slug] ?? {};
|
|
27072
27463
|
if (!Object.keys(over).length && !Object.keys(base).length) continue;
|
|
27073
27464
|
const merged = fillDefaults({
|
|
@@ -27078,32 +27469,32 @@ function migrateOnce() {
|
|
|
27078
27469
|
githubToken: over.githubToken || base.githubToken,
|
|
27079
27470
|
provider: over.provider || base.provider
|
|
27080
27471
|
});
|
|
27081
|
-
|
|
27472
|
+
import_fs11.default.mkdirSync(credsDir(slug), { recursive: true });
|
|
27082
27473
|
writeJson(githubPath(slug), merged);
|
|
27083
27474
|
}
|
|
27084
27475
|
let odFiles = [];
|
|
27085
27476
|
try {
|
|
27086
|
-
odFiles =
|
|
27477
|
+
odFiles = import_fs11.default.readdirSync(CONFIG_DIR).filter((f) => f.startsWith("space-") && f.endsWith(".onedrive.json"));
|
|
27087
27478
|
} catch {
|
|
27088
27479
|
odFiles = [];
|
|
27089
27480
|
}
|
|
27090
27481
|
for (const name of odFiles) {
|
|
27091
27482
|
const slug = name.slice("space-".length, -".onedrive.json".length);
|
|
27092
|
-
if (
|
|
27483
|
+
if (import_fs11.default.existsSync(connectionPath(slug))) {
|
|
27093
27484
|
consumed.push(name);
|
|
27094
27485
|
continue;
|
|
27095
27486
|
}
|
|
27096
|
-
const conn = readLegacy(
|
|
27487
|
+
const conn = readLegacy(import_path23.default.join(CONFIG_DIR, name));
|
|
27097
27488
|
if (!conn.readable) continue;
|
|
27098
27489
|
if (conn.value) {
|
|
27099
|
-
|
|
27490
|
+
import_fs11.default.mkdirSync(credsDir(slug), { recursive: true });
|
|
27100
27491
|
writeJson(connectionPath(slug), conn.value);
|
|
27101
27492
|
}
|
|
27102
27493
|
consumed.push(name);
|
|
27103
27494
|
}
|
|
27104
27495
|
for (const name of [...consumed, "runner-config.json"]) {
|
|
27105
27496
|
try {
|
|
27106
|
-
|
|
27497
|
+
import_fs11.default.rmSync(import_path23.default.join(CONFIG_DIR, name), { force: true });
|
|
27107
27498
|
} catch {
|
|
27108
27499
|
}
|
|
27109
27500
|
}
|
|
@@ -27115,14 +27506,14 @@ function encryptStrayPlaintextConfigs() {
|
|
|
27115
27506
|
const OURS = /^(config|runner-config|space-.*)\.json(\.bak-\d+)?$/;
|
|
27116
27507
|
let names = [];
|
|
27117
27508
|
try {
|
|
27118
|
-
names =
|
|
27509
|
+
names = import_fs11.default.readdirSync(CONFIG_DIR).filter((f) => OURS.test(f));
|
|
27119
27510
|
} catch {
|
|
27120
27511
|
return;
|
|
27121
27512
|
}
|
|
27122
27513
|
for (const name of names) {
|
|
27123
|
-
const filePath =
|
|
27514
|
+
const filePath = import_path23.default.join(CONFIG_DIR, name);
|
|
27124
27515
|
try {
|
|
27125
|
-
const raw =
|
|
27516
|
+
const raw = import_fs11.default.readFileSync(filePath, "utf-8");
|
|
27126
27517
|
if (!raw.startsWith("{")) continue;
|
|
27127
27518
|
writeSecureJson(filePath, JSON.parse(raw));
|
|
27128
27519
|
} catch {
|
|
@@ -27145,7 +27536,7 @@ async function getConfiguredConnections() {
|
|
|
27145
27536
|
for (const slug of listConfiguredSlugs()) {
|
|
27146
27537
|
if (kinds.has("git") && kinds.has("onedrive")) break;
|
|
27147
27538
|
if (readBoard(slug).githubToken) kinds.add("git");
|
|
27148
|
-
if (
|
|
27539
|
+
if (import_fs11.default.existsSync(connectionPath(slug))) kinds.add("onedrive");
|
|
27149
27540
|
}
|
|
27150
27541
|
} catch {
|
|
27151
27542
|
return [];
|
|
@@ -27157,7 +27548,7 @@ function listConfiguredSlugs() {
|
|
|
27157
27548
|
try {
|
|
27158
27549
|
migrateToSpaceFolders();
|
|
27159
27550
|
return configDirNames().filter(
|
|
27160
|
-
(slug) =>
|
|
27551
|
+
(slug) => import_fs11.default.existsSync(githubPath(slug)) || import_fs11.default.existsSync(connectionPath(slug))
|
|
27161
27552
|
);
|
|
27162
27553
|
} catch {
|
|
27163
27554
|
return [];
|
|
@@ -27176,7 +27567,7 @@ var localConfigProvider = {
|
|
|
27176
27567
|
// which is what makes disconnect stick.
|
|
27177
27568
|
writeConfig(spaceSlug, config) {
|
|
27178
27569
|
migrateToSpaceFolders();
|
|
27179
|
-
|
|
27570
|
+
import_fs11.default.mkdirSync(credsDir(spaceSlug), { recursive: true });
|
|
27180
27571
|
writeJson(githubPath(spaceSlug), config);
|
|
27181
27572
|
},
|
|
27182
27573
|
readOneDriveConnection(spaceSlug) {
|
|
@@ -27190,7 +27581,7 @@ var localConfigProvider = {
|
|
|
27190
27581
|
writeOneDriveConnection(spaceSlug, connection) {
|
|
27191
27582
|
const filePath = connectionPath(spaceSlug);
|
|
27192
27583
|
if (!connection) {
|
|
27193
|
-
|
|
27584
|
+
import_fs11.default.rmSync(filePath, { force: true });
|
|
27194
27585
|
return;
|
|
27195
27586
|
}
|
|
27196
27587
|
writeJson(filePath, connection);
|
|
@@ -27213,9 +27604,10 @@ var localConfigProvider = {
|
|
|
27213
27604
|
"builds",
|
|
27214
27605
|
"secrets",
|
|
27215
27606
|
"claude",
|
|
27216
|
-
"codex"
|
|
27607
|
+
"codex",
|
|
27608
|
+
"scratchpad"
|
|
27217
27609
|
]) {
|
|
27218
|
-
|
|
27610
|
+
import_fs11.default.mkdirSync(import_path23.default.join(dir, sub), { recursive: true });
|
|
27219
27611
|
}
|
|
27220
27612
|
return dir;
|
|
27221
27613
|
}
|
|
@@ -27389,15 +27781,15 @@ async function startDaemon(config) {
|
|
|
27389
27781
|
}
|
|
27390
27782
|
);
|
|
27391
27783
|
const indexCandidates = [
|
|
27392
|
-
|
|
27393
|
-
|
|
27784
|
+
import_path24.default.join(__dirname, "..", "index"),
|
|
27785
|
+
import_path24.default.join(__dirname, "..", "..", "index", "image")
|
|
27394
27786
|
];
|
|
27395
|
-
const indexDir = indexCandidates.find((p) =>
|
|
27787
|
+
const indexDir = indexCandidates.find((p) => import_fs12.default.existsSync(p)) ?? indexCandidates[0];
|
|
27396
27788
|
const odIndex = new IndexService({
|
|
27397
27789
|
controlToken: (0, import_crypto11.createHash)("sha256").update(config.authToken).digest("hex"),
|
|
27398
27790
|
embedUrl: () => config.serverUrl,
|
|
27399
27791
|
indexDir,
|
|
27400
|
-
dataDir:
|
|
27792
|
+
dataDir: import_path24.default.join(CONFIG_DIR, "index-data")
|
|
27401
27793
|
});
|
|
27402
27794
|
void odIndex.ensureRunning().catch((err) => {
|
|
27403
27795
|
console.error(
|
|
@@ -28013,27 +28405,24 @@ ${err.fix}`);
|
|
|
28013
28405
|
console.error("");
|
|
28014
28406
|
process.exit(1);
|
|
28015
28407
|
}
|
|
28016
|
-
|
|
28017
|
-
|
|
28018
|
-
const
|
|
28019
|
-
|
|
28020
|
-
console.error(
|
|
28408
|
+
function warnUnsupportedNode() {
|
|
28409
|
+
if (indexNodeSupported()) return;
|
|
28410
|
+
const alternative = findIndexNode();
|
|
28411
|
+
console.warn(
|
|
28021
28412
|
`
|
|
28022
|
-
|
|
28023
|
-
The
|
|
28024
|
-
|
|
28025
|
-
|
|
28026
|
-
|
|
28027
|
-
`
|
|
28413
|
+
Node ${process.versions.node} is outside the range the OneDrive index supports (${MIN_INDEX_NODE_MAJOR}-${MAX_INDEX_NODE_MAJOR}, pinned 24.11.1).` + (alternative ? `
|
|
28414
|
+
The index will run under ${alternative}. Everything else is unaffected.
|
|
28415
|
+
` : `
|
|
28416
|
+
OneDrive search will be unavailable until a supported Node is installed (nvm install 24.11.1). Everything else works.
|
|
28417
|
+
`)
|
|
28028
28418
|
);
|
|
28029
|
-
process.exit(1);
|
|
28030
28419
|
}
|
|
28031
28420
|
import_node_dns.default.setDefaultResultOrder("ipv4first");
|
|
28032
28421
|
var program2 = new Command();
|
|
28033
28422
|
program2.name("factiii-runner").description("Factiii Runner - run Board AI agents on remote servers").version(currentVersion());
|
|
28034
28423
|
program2.command("setup").description("Verify the host toolchain and pair this runner").option("-s, --server <url>", "API server URL", "https://api.factiii.com").action(async (opts) => {
|
|
28035
28424
|
try {
|
|
28036
|
-
|
|
28425
|
+
warnUnsupportedNode();
|
|
28037
28426
|
await setup(opts.server);
|
|
28038
28427
|
} catch (err) {
|
|
28039
28428
|
console.error(
|
|
@@ -28062,7 +28451,7 @@ program2.command("connect").description("Pair this runner with a Factiii account
|
|
|
28062
28451
|
}
|
|
28063
28452
|
});
|
|
28064
28453
|
program2.command("start").description("Start the runner daemon").action(async () => {
|
|
28065
|
-
|
|
28454
|
+
warnUnsupportedNode();
|
|
28066
28455
|
if (!configExists()) {
|
|
28067
28456
|
console.error('No config found. Run "factiii-runner setup" first.');
|
|
28068
28457
|
process.exit(1);
|
|
@@ -28108,8 +28497,7 @@ program2.command("status").description("Show current runner configuration").acti
|
|
|
28108
28497
|
console.log(`Token: \xB7\xB7\xB7\xB7${config.authToken.slice(-4)}`);
|
|
28109
28498
|
});
|
|
28110
28499
|
program2.command("doctor").description("Show where each host tool resolves, for a failed environment").action(async () => {
|
|
28111
|
-
const
|
|
28112
|
-
const nodeNote = nodeMajor > MAX_SUPPORTED_NODE_MAJOR ? " (UNSUPPORTED \u2014 use 24.11.1)" : "";
|
|
28500
|
+
const nodeNote = indexNodeSupported() ? "" : ` (OneDrive index needs ${MIN_INDEX_NODE_MAJOR}-${MAX_INDEX_NODE_MAJOR}; ${findIndexNode() ?? "no supported node found"})`;
|
|
28113
28501
|
console.log(`${"node".padEnd(14)} ${process.execPath} v${process.versions.node}${nodeNote}`);
|
|
28114
28502
|
const report = await reportToolchain(hostTarget(CONFIG_DIR));
|
|
28115
28503
|
for (const tool of report.tools) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: scratchpad
|
|
3
|
+
description: Show the user something, or hand them a file, through the board's Scratchpad panel - by writing to $SCRATCHPAD. Anything you put there renders beside the terminal (image, screenshot, chart, video, audio, page, link, text) and can be saved to their machine or phone. TRIGGER when the user asks to see, show, preview, display or look at something; when you produce an image/diagram/chart/screenshot/recording; when you have a URL worth opening; when an answer is easier to look at than to read in a terminal; and whenever you need to GIVE the user a file - an export, a report, a bundle, a CSV, a log, a dump, anything they asked you to generate, save, download or send them. Writing it to $SCRATCHPAD is how a file gets off the runner and to the user.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# scratchpad
|
|
7
|
+
|
|
8
|
+
The terminal you are running in has a panel beside it. Anything you put in the
|
|
9
|
+
`$SCRATCHPAD` directory shows up there, drawn rather than printed: images
|
|
10
|
+
render, video and audio play, links become clickable, text and markdown are
|
|
11
|
+
shown as they are.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cp chart.png "$SCRATCHPAD/"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That is the whole interface. There is no command to run, nothing to install,
|
|
18
|
+
and nothing to tell the panel: it is watching the directory and updates on
|
|
19
|
+
its own within a second.
|
|
20
|
+
|
|
21
|
+
## What renders as what
|
|
22
|
+
|
|
23
|
+
The **extension decides**, so name the file for what it is:
|
|
24
|
+
|
|
25
|
+
| Write | You get |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `.png .jpg .gif .webp .svg` | the image, click-to-open-full-size |
|
|
28
|
+
| `.mp4 .webm .mov` | a video player |
|
|
29
|
+
| `.mp3 .wav .m4a .ogg` | an audio player |
|
|
30
|
+
| `.html` | the page itself, rendered in a sandboxed frame |
|
|
31
|
+
| `.md` `.txt` and anything unlisted | the text as-is |
|
|
32
|
+
| `.url` | a clickable link (put the URL on the first line) |
|
|
33
|
+
|
|
34
|
+
A link:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
echo "https://example.com/the-thing" > "$SCRATCHPAD/preview.url"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
A note, when the answer is a shape rather than a sentence:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cat > "$SCRATCHPAD/rollout-plan.md" <<'MD'
|
|
44
|
+
# Rollout
|
|
45
|
+
1. staging, behind the flag
|
|
46
|
+
2. 10% of prod for a day
|
|
47
|
+
3. everyone
|
|
48
|
+
MD
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Handing a file to the user
|
|
52
|
+
|
|
53
|
+
Every item on the panel has a Save button, and the panel pulls the file off the
|
|
54
|
+
runner for them. So the scratchpad is not only for looking at: it is how you
|
|
55
|
+
give someone a file.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cp coverage-report.csv "$SCRATCHPAD/"
|
|
59
|
+
zip -qr "$SCRATCHPAD/failing-run-logs.zip" logs/
|
|
60
|
+
pg_dump "$DATABASE_URL" > "$SCRATCHPAD/schema-before-migration.sql"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A file meant for keeping does not have to render well. A `.zip` shows as
|
|
64
|
+
nothing much and a `.csv` as raw text, which is fine, because the user is going
|
|
65
|
+
to save it rather than read it in a narrow column. Name it for what it is so
|
|
66
|
+
they know what they are saving, and say in one line that it is there.
|
|
67
|
+
|
|
68
|
+
Nothing else you can do reaches the user's disk. If you write a file into the
|
|
69
|
+
workspace and tell them where it is, they have to go and get it themselves,
|
|
70
|
+
which on a phone means they cannot.
|
|
71
|
+
|
|
72
|
+
## Rules
|
|
73
|
+
|
|
74
|
+
- **Flat, plain names.** Letters, numbers, dots, dashes, spaces and
|
|
75
|
+
underscores; no subdirectories, and never a leading dot. A name the panel
|
|
76
|
+
cannot show is a drawing the user never sees.
|
|
77
|
+
- **Newest is shown first.** Rewriting a file under the same name replaces its
|
|
78
|
+
card and moves it to the top, which is how you update a drawing rather than
|
|
79
|
+
pile up `chart-2.png`, `chart-3.png`.
|
|
80
|
+
- **Under 12MB to be shown inline.** A bigger file is still listed, and the
|
|
81
|
+
user can download it from the panel, but it will not render on the stage.
|
|
82
|
+
Trim a recording if you want it watched rather than saved.
|
|
83
|
+
- **The board is shared.** One scratchpad per board, not per session, and it
|
|
84
|
+
survives the session that drew on it. Clean up what has gone stale:
|
|
85
|
+
`rm "$SCRATCHPAD/old-thing.png"`.
|
|
86
|
+
|
|
87
|
+
## When to reach for it
|
|
88
|
+
|
|
89
|
+
Use it whenever the answer is easier to look at than to read: a screenshot of
|
|
90
|
+
the change you just made, a generated chart, a diagram, a recorded repro, a
|
|
91
|
+
preview URL you just exposed, a table too wide for an 80-column shell.
|
|
92
|
+
|
|
93
|
+
And use it for anything the user is meant to end up holding: the export they
|
|
94
|
+
asked for, the report you generated, the logs from the run that failed, the
|
|
95
|
+
backup you just took.
|
|
96
|
+
|
|
97
|
+
Do not narrate it into the terminal as well. Write the file and say, in one
|
|
98
|
+
line, what you put on the scratchpad - the user is looking at the panel.
|