@factiii/runner 0.12.2 → 0.13.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/dist/cli.js +290 -8
- package/package.json +1 -1
- package/plugin/factiii/skills/scratchpad/SKILL.md +98 -0
package/dist/cli.js
CHANGED
|
@@ -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
|
}
|
|
@@ -17680,6 +17685,8 @@ var NOISE_GLOBS = ["**/.DS_Store", "**/4913", "**/*~", "**/.#*", "**/#*#"];
|
|
|
17680
17685
|
var MAX_BATCH = 200;
|
|
17681
17686
|
var FLUSH_MS = 120;
|
|
17682
17687
|
var SELF_WRITE_MS = 2e3;
|
|
17688
|
+
var RETRY_MIN_MS = 500;
|
|
17689
|
+
var RETRY_MAX_MS = 15e3;
|
|
17683
17690
|
var dynamicImport = new Function("s", "return import(s)");
|
|
17684
17691
|
var importFs = new Function("s", "return import(s)");
|
|
17685
17692
|
async function resolveReal(path23) {
|
|
@@ -17710,13 +17717,16 @@ var WorkspaceWatcher = class {
|
|
|
17710
17717
|
* A no-git card's root is the space dir itself, which also
|
|
17711
17718
|
* holds the runner's own bookkeeping.
|
|
17712
17719
|
*/
|
|
17713
|
-
constructor(root, emit, exclude = []) {
|
|
17720
|
+
constructor(root, emit, exclude = [], subscribeImpl) {
|
|
17714
17721
|
this.root = root;
|
|
17715
17722
|
this.emit = emit;
|
|
17716
17723
|
this.exclude = exclude;
|
|
17724
|
+
this.subscribeImpl = subscribeImpl;
|
|
17717
17725
|
this.sub = null;
|
|
17718
17726
|
this.pending = emptyPending();
|
|
17719
17727
|
this.flushTimer = null;
|
|
17728
|
+
this.retryTimer = null;
|
|
17729
|
+
this.retryDelay = RETRY_MIN_MS;
|
|
17720
17730
|
/** path -> expiry. Events for these are ours (the editor just saved) and must
|
|
17721
17731
|
* not come back as "changed on disk". */
|
|
17722
17732
|
this.selfWrites = /* @__PURE__ */ new Map();
|
|
@@ -17726,11 +17736,11 @@ var WorkspaceWatcher = class {
|
|
|
17726
17736
|
/** Fire-and-forget: callers treat starting a watch as synchronous, and
|
|
17727
17737
|
* nothing can be emitted before `subscribe` resolves anyway. */
|
|
17728
17738
|
start() {
|
|
17729
|
-
if (this.sub || this.closed) return;
|
|
17739
|
+
if (this.sub || this.closed || this.retryTimer) return;
|
|
17730
17740
|
void this.open();
|
|
17731
17741
|
}
|
|
17732
17742
|
async open() {
|
|
17733
|
-
const subscribe = await getSubscribe();
|
|
17743
|
+
const subscribe = this.subscribeImpl ?? await getSubscribe();
|
|
17734
17744
|
if (!subscribe) return this.stop();
|
|
17735
17745
|
this.watchRoot = await resolveReal(this.root);
|
|
17736
17746
|
const exclude = await Promise.all(this.exclude.map(resolveReal));
|
|
@@ -17738,7 +17748,7 @@ var WorkspaceWatcher = class {
|
|
|
17738
17748
|
const sub = await subscribe(
|
|
17739
17749
|
this.watchRoot,
|
|
17740
17750
|
(err, events) => {
|
|
17741
|
-
if (err) return this.
|
|
17751
|
+
if (err) return this.retry();
|
|
17742
17752
|
for (const event of events) this.onEvent(event);
|
|
17743
17753
|
},
|
|
17744
17754
|
// Prunes the walk itself, so a pruned tree is never enumerated.
|
|
@@ -17755,10 +17765,39 @@ var WorkspaceWatcher = class {
|
|
|
17755
17765
|
return;
|
|
17756
17766
|
}
|
|
17757
17767
|
this.sub = sub;
|
|
17768
|
+
this.retryDelay = RETRY_MIN_MS;
|
|
17758
17769
|
} catch {
|
|
17759
|
-
this.
|
|
17770
|
+
this.retry();
|
|
17760
17771
|
}
|
|
17761
17772
|
}
|
|
17773
|
+
/**
|
|
17774
|
+
* Re-subscribe after the OS dropped the watch — a remount, a wake from
|
|
17775
|
+
* sleep, an inotify limit easing. Treating those as terminal left a runner
|
|
17776
|
+
* watching nothing for the rest of its life. `resync` on the way back up,
|
|
17777
|
+
* since whatever changed while it was down produced no events.
|
|
17778
|
+
*/
|
|
17779
|
+
retry() {
|
|
17780
|
+
if (this.closed || this.retryTimer) return;
|
|
17781
|
+
const sub = this.sub;
|
|
17782
|
+
this.sub = null;
|
|
17783
|
+
if (sub) void sub.unsubscribe().catch(() => void 0);
|
|
17784
|
+
const delay = this.retryDelay;
|
|
17785
|
+
this.retryDelay = Math.min(this.retryDelay * 2, RETRY_MAX_MS);
|
|
17786
|
+
this.retryTimer = setTimeout(() => {
|
|
17787
|
+
this.retryTimer = null;
|
|
17788
|
+
if (this.closed) return;
|
|
17789
|
+
void this.open().then(() => {
|
|
17790
|
+
if (this.sub) {
|
|
17791
|
+
this.emit({
|
|
17792
|
+
added: [],
|
|
17793
|
+
changed: [],
|
|
17794
|
+
removed: [],
|
|
17795
|
+
resync: true
|
|
17796
|
+
});
|
|
17797
|
+
}
|
|
17798
|
+
});
|
|
17799
|
+
}, delay);
|
|
17800
|
+
}
|
|
17762
17801
|
/** Give up and say so once, so the UI falls back to manual refresh rather
|
|
17763
17802
|
* than showing a silently frozen tree. */
|
|
17764
17803
|
stop() {
|
|
@@ -17794,6 +17833,10 @@ var WorkspaceWatcher = class {
|
|
|
17794
17833
|
this.flushTimer = null;
|
|
17795
17834
|
}
|
|
17796
17835
|
this.pending = emptyPending();
|
|
17836
|
+
if (this.retryTimer) {
|
|
17837
|
+
clearTimeout(this.retryTimer);
|
|
17838
|
+
this.retryTimer = null;
|
|
17839
|
+
}
|
|
17797
17840
|
this.selfWrites.clear();
|
|
17798
17841
|
const sub = this.sub;
|
|
17799
17842
|
this.sub = null;
|
|
@@ -18013,6 +18056,7 @@ var TerminalModeEngine = class {
|
|
|
18013
18056
|
paths.worktrees,
|
|
18014
18057
|
paths.repo,
|
|
18015
18058
|
paths.workspace,
|
|
18059
|
+
paths.scratchpad,
|
|
18016
18060
|
`${paths.root}/drops`
|
|
18017
18061
|
];
|
|
18018
18062
|
const watcher = new WorkspaceWatcher(
|
|
@@ -19854,6 +19898,204 @@ async function getDriveRoot(accessToken) {
|
|
|
19854
19898
|
};
|
|
19855
19899
|
}
|
|
19856
19900
|
|
|
19901
|
+
// ../../shared/all/helpers/scratchpad.ts
|
|
19902
|
+
var SCRATCHPAD_MAX_BYTES = 12e6;
|
|
19903
|
+
var SCRATCHPAD_MAX_ITEMS = 200;
|
|
19904
|
+
var SCRATCHPAD_CHUNK_BYTES = 512 * 1024;
|
|
19905
|
+
var SCRATCHPAD_MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;
|
|
19906
|
+
var KIND_BY_EXT = {
|
|
19907
|
+
png: "image",
|
|
19908
|
+
jpg: "image",
|
|
19909
|
+
jpeg: "image",
|
|
19910
|
+
gif: "image",
|
|
19911
|
+
webp: "image",
|
|
19912
|
+
avif: "image",
|
|
19913
|
+
bmp: "image",
|
|
19914
|
+
svg: "image",
|
|
19915
|
+
mp4: "video",
|
|
19916
|
+
webm: "video",
|
|
19917
|
+
mov: "video",
|
|
19918
|
+
m4v: "video",
|
|
19919
|
+
mp3: "audio",
|
|
19920
|
+
wav: "audio",
|
|
19921
|
+
m4a: "audio",
|
|
19922
|
+
ogg: "audio",
|
|
19923
|
+
aac: "audio",
|
|
19924
|
+
flac: "audio",
|
|
19925
|
+
pdf: "pdf",
|
|
19926
|
+
html: "html",
|
|
19927
|
+
htm: "html",
|
|
19928
|
+
md: "markdown",
|
|
19929
|
+
markdown: "markdown",
|
|
19930
|
+
url: "link"
|
|
19931
|
+
};
|
|
19932
|
+
function scratchpadExt(name) {
|
|
19933
|
+
const dot = name.lastIndexOf(".");
|
|
19934
|
+
if (dot <= 0 || dot === name.length - 1) return "";
|
|
19935
|
+
return name.slice(dot + 1).toLowerCase();
|
|
19936
|
+
}
|
|
19937
|
+
function scratchpadKind(name) {
|
|
19938
|
+
return KIND_BY_EXT[scratchpadExt(name)] ?? "text";
|
|
19939
|
+
}
|
|
19940
|
+
function isSafeScratchpadName(name) {
|
|
19941
|
+
if (!name || name.length > 255) return false;
|
|
19942
|
+
if (name.startsWith(".")) return false;
|
|
19943
|
+
return /^[A-Za-z0-9][A-Za-z0-9._ -]*$/.test(name);
|
|
19944
|
+
}
|
|
19945
|
+
|
|
19946
|
+
// ../../shared/all/helpers/board-agent-core/scratchpad-store.ts
|
|
19947
|
+
function shEscape2(s) {
|
|
19948
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
19949
|
+
}
|
|
19950
|
+
var STAT_META = `stat -f '%m %z' "$f" 2>/dev/null || stat -c '%Y %s' "$f" 2>/dev/null`;
|
|
19951
|
+
var TOO_LARGE = "Scratchpad item is too large to open here.";
|
|
19952
|
+
var ScratchpadStore = class {
|
|
19953
|
+
/**
|
|
19954
|
+
* @param target Resolved per call, never held: the space's exec target is
|
|
19955
|
+
* rebuilt as the space provisions, and a captured one would go
|
|
19956
|
+
* on pointing at the tree it was built for.
|
|
19957
|
+
* @param emit Tells connected clients the directory moved. Carries a
|
|
19958
|
+
* timestamp only: the panel refetches the whole listing.
|
|
19959
|
+
*/
|
|
19960
|
+
constructor(target, emit) {
|
|
19961
|
+
this.target = target;
|
|
19962
|
+
this.emit = emit;
|
|
19963
|
+
this.watcher = null;
|
|
19964
|
+
}
|
|
19965
|
+
dir() {
|
|
19966
|
+
return this.target().paths.scratchpad;
|
|
19967
|
+
}
|
|
19968
|
+
/**
|
|
19969
|
+
* Everything on the scratchpad, newest first.
|
|
19970
|
+
*
|
|
19971
|
+
* Dotfiles are skipped: the panel is a display surface, and `.DS_Store` is
|
|
19972
|
+
* not something anyone drew. So are subdirectories: the scratchpad is flat,
|
|
19973
|
+
* which is what lets a name be an id everywhere else in this file.
|
|
19974
|
+
*/
|
|
19975
|
+
async list() {
|
|
19976
|
+
const dir = this.dir();
|
|
19977
|
+
const out = await this.target().run([
|
|
19978
|
+
"sh",
|
|
19979
|
+
"-c",
|
|
19980
|
+
`mkdir -p ${shEscape2(dir)} && cd ${shEscape2(dir)} && for f in *; do
|
|
19981
|
+
[ -f "$f" ] || continue
|
|
19982
|
+
meta=$(${STAT_META}) || continue
|
|
19983
|
+
echo "$meta $f"
|
|
19984
|
+
done | LC_ALL=C sort -rn | head -n ${SCRATCHPAD_MAX_ITEMS}`
|
|
19985
|
+
]);
|
|
19986
|
+
this.startWatching();
|
|
19987
|
+
const items = [];
|
|
19988
|
+
for (const line of out.split("\n")) {
|
|
19989
|
+
const match = /^(\d+) (\d+) (.+)$/.exec(line);
|
|
19990
|
+
if (!match) continue;
|
|
19991
|
+
const [, mtime, size, name] = match;
|
|
19992
|
+
if (!isSafeScratchpadName(name)) continue;
|
|
19993
|
+
const bytes = Number(size);
|
|
19994
|
+
items.push({
|
|
19995
|
+
name,
|
|
19996
|
+
kind: scratchpadKind(name),
|
|
19997
|
+
size: bytes,
|
|
19998
|
+
// stat reports seconds; every other timestamp on the wire is ms.
|
|
19999
|
+
modifiedAt: Number(mtime) * 1e3,
|
|
20000
|
+
tooLarge: bytes > SCRATCHPAD_MAX_BYTES
|
|
20001
|
+
});
|
|
20002
|
+
}
|
|
20003
|
+
return items;
|
|
20004
|
+
}
|
|
20005
|
+
/**
|
|
20006
|
+
* One item's bytes, base64. Capped: past SCRATCHPAD_MAX_BYTES the panel is
|
|
20007
|
+
* told to show the path instead, so a 4GB screen recording cannot pin the
|
|
20008
|
+
* data channel for minutes.
|
|
20009
|
+
*
|
|
20010
|
+
* One shell, not a size check and then a read: the panel opens every card at
|
|
20011
|
+
* once, so a second spawn per item is a doubled cost on the whole listing -
|
|
20012
|
+
* and a file that changed between the two calls would have been read past
|
|
20013
|
+
* the cap anyway.
|
|
20014
|
+
*/
|
|
20015
|
+
async read(name) {
|
|
20016
|
+
const file = shEscape2(this.resolve(name));
|
|
20017
|
+
const content = await this.target().sh(
|
|
20018
|
+
// Unquoted `$(wc -c)` on purpose: BSD wc pads its count with spaces,
|
|
20019
|
+
// which `[ -gt ]` will not parse. `base64 -w 0` is GNU-only (BSD
|
|
20020
|
+
// spells it -b), so unwrap with tr rather than asking it not to wrap.
|
|
20021
|
+
`[ -f ${file} ] || exit 1
|
|
20022
|
+
if [ $(wc -c < ${file}) -gt ${SCRATCHPAD_MAX_BYTES} ]; then
|
|
20023
|
+
echo '${TOO_LARGE}' >&2; exit 1
|
|
20024
|
+
fi
|
|
20025
|
+
base64 < ${file} | tr -d '
|
|
20026
|
+
'`
|
|
20027
|
+
).catch((err) => {
|
|
20028
|
+
const text = err instanceof Error ? err.message : "";
|
|
20029
|
+
throw new Error(
|
|
20030
|
+
text.includes(TOO_LARGE) ? TOO_LARGE : "Could not read that item."
|
|
20031
|
+
);
|
|
20032
|
+
});
|
|
20033
|
+
return { content: content.trim(), encoding: "base64" };
|
|
20034
|
+
}
|
|
20035
|
+
/**
|
|
20036
|
+
* One byte range of an item, base64. What a download is built out of.
|
|
20037
|
+
*
|
|
20038
|
+
* No size cap here, unlike `read`: the cap exists because a preview holds
|
|
20039
|
+
* the whole file in one message, and a range does not. `tail | head` rather
|
|
20040
|
+
* than `dd`, which needs a GNU-only flag to promise a full block over a pipe.
|
|
20041
|
+
*/
|
|
20042
|
+
async readChunk(name, offset, length) {
|
|
20043
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
20044
|
+
throw new Error("Invalid scratchpad offset.");
|
|
20045
|
+
}
|
|
20046
|
+
if (!Number.isInteger(length) || length <= 0 || length > SCRATCHPAD_CHUNK_BYTES) {
|
|
20047
|
+
throw new Error("Invalid scratchpad chunk length.");
|
|
20048
|
+
}
|
|
20049
|
+
const file = shEscape2(this.resolve(name));
|
|
20050
|
+
const content = await this.target().sh(
|
|
20051
|
+
// `tail -c +N` counts from 1, so the first byte is offset 0 plus one.
|
|
20052
|
+
`[ -f ${file} ] || exit 1
|
|
20053
|
+
tail -c +${offset + 1} ${file} | head -c ${length} | base64 | tr -d '
|
|
20054
|
+
'`
|
|
20055
|
+
).catch(() => {
|
|
20056
|
+
throw new Error("Could not read that item.");
|
|
20057
|
+
});
|
|
20058
|
+
return { content: content.trim(), encoding: "base64" };
|
|
20059
|
+
}
|
|
20060
|
+
async remove(name) {
|
|
20061
|
+
await this.target().run(["rm", "-f", "--", this.resolve(name)]);
|
|
20062
|
+
}
|
|
20063
|
+
/** Files only, and only at the top level: whatever put a directory in there
|
|
20064
|
+
* is not something this panel drew, and `rm -rf` on an agent-writable path
|
|
20065
|
+
* is a worse tool than this job needs. */
|
|
20066
|
+
async clear() {
|
|
20067
|
+
const dir = shEscape2(this.dir());
|
|
20068
|
+
await this.target().run([
|
|
20069
|
+
"sh",
|
|
20070
|
+
"-c",
|
|
20071
|
+
`mkdir -p ${dir} && cd ${dir} && find . -maxdepth 1 -type f -delete`
|
|
20072
|
+
]);
|
|
20073
|
+
}
|
|
20074
|
+
destroy() {
|
|
20075
|
+
const watcher = this.watcher;
|
|
20076
|
+
this.watcher = null;
|
|
20077
|
+
if (watcher) void watcher.close();
|
|
20078
|
+
}
|
|
20079
|
+
/** Idempotent: every panel that opens calls `list`, and they must not stack
|
|
20080
|
+
* watchers on one directory. */
|
|
20081
|
+
startWatching() {
|
|
20082
|
+
if (this.watcher) return;
|
|
20083
|
+
const watcher = new WorkspaceWatcher(this.dir(), () => {
|
|
20084
|
+
this.emit({ at: Date.now() });
|
|
20085
|
+
});
|
|
20086
|
+
this.watcher = watcher;
|
|
20087
|
+
watcher.start();
|
|
20088
|
+
}
|
|
20089
|
+
/** Absolute path for a name the agent chose. Flat by construction: the name
|
|
20090
|
+
* is validated, never joined with anything a caller supplied. */
|
|
20091
|
+
resolve(name) {
|
|
20092
|
+
if (!isSafeScratchpadName(name)) {
|
|
20093
|
+
throw new Error("Invalid scratchpad item name.");
|
|
20094
|
+
}
|
|
20095
|
+
return `${this.dir()}/${name}`;
|
|
20096
|
+
}
|
|
20097
|
+
};
|
|
20098
|
+
|
|
19857
20099
|
// ../../shared/all/helpers/board-agent-core/space-core.ts
|
|
19858
20100
|
var import_promises10 = require("fs/promises");
|
|
19859
20101
|
var import_path14 = __toESM(require("path"));
|
|
@@ -20369,7 +20611,7 @@ var SpaceCore = class {
|
|
|
20369
20611
|
status("Preparing workspace\u2026");
|
|
20370
20612
|
const { paths } = target;
|
|
20371
20613
|
await target.sh(
|
|
20372
|
-
`mkdir -p ${paths.root} ${paths.worktrees} ${paths.state} ${paths.backups} ${paths.builds} ${paths.bin}`
|
|
20614
|
+
`mkdir -p ${paths.root} ${paths.worktrees} ${paths.state} ${paths.backups} ${paths.builds} ${paths.bin} ${paths.scratchpad}`
|
|
20373
20615
|
);
|
|
20374
20616
|
await allocateSpacePorts(this.spaceDir());
|
|
20375
20617
|
await this.ensureSecretsBridge().catch(() => void 0);
|
|
@@ -20712,6 +20954,10 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
20712
20954
|
this.emitter = options.emitter;
|
|
20713
20955
|
this.claude = new ClaudeModeEngine(this.core, this.emitter);
|
|
20714
20956
|
this.terminal = new TerminalModeEngine(this.core, this.emitter);
|
|
20957
|
+
this.scratchpad = new ScratchpadStore(
|
|
20958
|
+
() => this.core.target(),
|
|
20959
|
+
(payload) => this.emitter.emitScratchpad(payload)
|
|
20960
|
+
);
|
|
20715
20961
|
this.gate.subscribe(() => this.projectGateOntoRun());
|
|
20716
20962
|
}
|
|
20717
20963
|
/** Everything the runner does for this space that isn't a card session, so
|
|
@@ -21688,6 +21934,22 @@ var BoardAgentEngine = class _BoardAgentEngine {
|
|
|
21688
21934
|
bareListExposed(p) {
|
|
21689
21935
|
return this.terminal.bareListExposed(p);
|
|
21690
21936
|
}
|
|
21937
|
+
// ── Scratchpad (→ the space's display surface) ──
|
|
21938
|
+
scratchpadList() {
|
|
21939
|
+
return this.scratchpad.list();
|
|
21940
|
+
}
|
|
21941
|
+
scratchpadRead(p) {
|
|
21942
|
+
return this.scratchpad.read(p.name);
|
|
21943
|
+
}
|
|
21944
|
+
scratchpadReadChunk(p) {
|
|
21945
|
+
return this.scratchpad.readChunk(p.name, p.offset, p.length);
|
|
21946
|
+
}
|
|
21947
|
+
scratchpadDelete(p) {
|
|
21948
|
+
return this.scratchpad.remove(p.name);
|
|
21949
|
+
}
|
|
21950
|
+
scratchpadClear() {
|
|
21951
|
+
return this.scratchpad.clear();
|
|
21952
|
+
}
|
|
21691
21953
|
// ── Cross-mode coordinators ──
|
|
21692
21954
|
async cardExecute(payload) {
|
|
21693
21955
|
const {
|
|
@@ -22017,6 +22279,7 @@ ${c.content.trim() || "(no description)"}`
|
|
|
22017
22279
|
void this.codexLogin?.cancel();
|
|
22018
22280
|
this.claude.destroy();
|
|
22019
22281
|
this.terminal.destroy();
|
|
22282
|
+
this.scratchpad.destroy();
|
|
22020
22283
|
this.core.destroy();
|
|
22021
22284
|
}
|
|
22022
22285
|
/** Route one action/payload to the right engine method, for both transports.
|
|
@@ -22347,6 +22610,24 @@ ${c.content.trim() || "(no description)"}`
|
|
|
22347
22610
|
return await this.bareListExposed(
|
|
22348
22611
|
payload
|
|
22349
22612
|
);
|
|
22613
|
+
case "scratchpadList":
|
|
22614
|
+
return await this.scratchpadList();
|
|
22615
|
+
case "scratchpadRead":
|
|
22616
|
+
return await this.scratchpadRead(
|
|
22617
|
+
payload
|
|
22618
|
+
);
|
|
22619
|
+
case "scratchpadReadChunk":
|
|
22620
|
+
return await this.scratchpadReadChunk(
|
|
22621
|
+
payload
|
|
22622
|
+
);
|
|
22623
|
+
case "scratchpadDelete":
|
|
22624
|
+
await this.scratchpadDelete(
|
|
22625
|
+
payload
|
|
22626
|
+
);
|
|
22627
|
+
return void 0;
|
|
22628
|
+
case "scratchpadClear":
|
|
22629
|
+
await this.scratchpadClear();
|
|
22630
|
+
return void 0;
|
|
22350
22631
|
case "secureStatus":
|
|
22351
22632
|
return this.secureStatus();
|
|
22352
22633
|
default:
|
|
@@ -27361,7 +27642,8 @@ var localConfigProvider = {
|
|
|
27361
27642
|
"builds",
|
|
27362
27643
|
"secrets",
|
|
27363
27644
|
"claude",
|
|
27364
|
-
"codex"
|
|
27645
|
+
"codex",
|
|
27646
|
+
"scratchpad"
|
|
27365
27647
|
]) {
|
|
27366
27648
|
import_fs11.default.mkdirSync(import_path23.default.join(dir, sub), { recursive: true });
|
|
27367
27649
|
}
|
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.
|