@standardagents/code 0.9.1 → 0.9.2
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/index.js +371 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -409,6 +409,21 @@ var ApiClient = class {
|
|
|
409
409
|
} catch {
|
|
410
410
|
}
|
|
411
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* Run a user-typed `!command` on the thread's execution owner (wherever the
|
|
414
|
+
* session runs — e.g. a remote VPS daemon). The instance forwards it over
|
|
415
|
+
* the bridge, records it in history, and returns the combined output.
|
|
416
|
+
*/
|
|
417
|
+
async runCommand(threadId, command) {
|
|
418
|
+
try {
|
|
419
|
+
return await this.json(
|
|
420
|
+
`/api/threads/${threadId}/run_command`,
|
|
421
|
+
{ method: "POST", body: JSON.stringify({ command }) }
|
|
422
|
+
);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
412
427
|
/** The thread's current goal (set via set_goal / update_goal_step). */
|
|
413
428
|
async getGoal(threadId) {
|
|
414
429
|
try {
|
|
@@ -1208,9 +1223,9 @@ function setMcpServerEnabled(name, enabled) {
|
|
|
1208
1223
|
write(cfg);
|
|
1209
1224
|
}
|
|
1210
1225
|
function write(cfg) {
|
|
1211
|
-
const
|
|
1212
|
-
fs4.mkdirSync(path3.dirname(
|
|
1213
|
-
fs4.writeFileSync(
|
|
1226
|
+
const file2 = configFile();
|
|
1227
|
+
fs4.mkdirSync(path3.dirname(file2), { recursive: true });
|
|
1228
|
+
fs4.writeFileSync(file2, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
1214
1229
|
}
|
|
1215
1230
|
function parseServerSpec(spec) {
|
|
1216
1231
|
const trimmed = spec.trim();
|
|
@@ -1349,12 +1364,12 @@ var HostTools = class {
|
|
|
1349
1364
|
}
|
|
1350
1365
|
}
|
|
1351
1366
|
async readFile(args) {
|
|
1352
|
-
const
|
|
1353
|
-
const stat = await fsp.stat(
|
|
1367
|
+
const file2 = this.resolve(String(args.path || ""));
|
|
1368
|
+
const stat = await fsp.stat(file2).catch(() => null);
|
|
1354
1369
|
if (!stat) return { ok: false, error: `File not found: ${args.path}` };
|
|
1355
1370
|
if (stat.isDirectory()) return { ok: false, error: `${args.path} is a directory` };
|
|
1356
1371
|
if (stat.size > 2e6) return { ok: false, error: `File too large (${stat.size} bytes)` };
|
|
1357
|
-
const content = await fsp.readFile(
|
|
1372
|
+
const content = await fsp.readFile(file2, "utf8");
|
|
1358
1373
|
const lines = content.split("\n");
|
|
1359
1374
|
const offset = typeof args.offset === "number" ? Math.max(1, args.offset) : 1;
|
|
1360
1375
|
const limit = typeof args.limit === "number" ? args.limit : lines.length;
|
|
@@ -1390,24 +1405,24 @@ var HostTools = class {
|
|
|
1390
1405
|
return { ok: true, result: rel.join("\n") || "(no files)" };
|
|
1391
1406
|
}
|
|
1392
1407
|
async writeFile(args) {
|
|
1393
|
-
const
|
|
1408
|
+
const file2 = this.resolve(String(args.path || ""));
|
|
1394
1409
|
const content = String(args.content ?? "");
|
|
1395
|
-
await fsp.mkdir(path3.dirname(
|
|
1396
|
-
const existed = fs4.existsSync(
|
|
1397
|
-
await fsp.writeFile(
|
|
1410
|
+
await fsp.mkdir(path3.dirname(file2), { recursive: true });
|
|
1411
|
+
const existed = fs4.existsSync(file2);
|
|
1412
|
+
await fsp.writeFile(file2, content, "utf8");
|
|
1398
1413
|
return {
|
|
1399
1414
|
ok: true,
|
|
1400
|
-
result: `${existed ? "Overwrote" : "Created"} ${path3.relative(this.projectDir,
|
|
1415
|
+
result: `${existed ? "Overwrote" : "Created"} ${path3.relative(this.projectDir, file2)} (${Buffer.byteLength(content)} bytes)`
|
|
1401
1416
|
};
|
|
1402
1417
|
}
|
|
1403
1418
|
async editFile(args) {
|
|
1404
|
-
const
|
|
1419
|
+
const file2 = this.resolve(String(args.path || ""));
|
|
1405
1420
|
const oldStr = String(args.old_string ?? "");
|
|
1406
1421
|
const newStr = String(args.new_string ?? "");
|
|
1407
1422
|
const replaceAll = args.replace_all === true;
|
|
1408
|
-
const stat = await fsp.stat(
|
|
1423
|
+
const stat = await fsp.stat(file2).catch(() => null);
|
|
1409
1424
|
if (!stat) return { ok: false, error: `File not found: ${args.path}` };
|
|
1410
|
-
const content = await fsp.readFile(
|
|
1425
|
+
const content = await fsp.readFile(file2, "utf8");
|
|
1411
1426
|
if (oldStr === "") return { ok: false, error: "old_string cannot be empty" };
|
|
1412
1427
|
const count = content.split(oldStr).length - 1;
|
|
1413
1428
|
if (count === 0) return { ok: false, error: "old_string not found in file (it must match exactly)." };
|
|
@@ -1415,8 +1430,8 @@ var HostTools = class {
|
|
|
1415
1430
|
return { ok: false, error: `old_string is not unique (${count} matches). Add more context or set replace_all.` };
|
|
1416
1431
|
}
|
|
1417
1432
|
const updated = replaceAll ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
|
|
1418
|
-
await fsp.writeFile(
|
|
1419
|
-
return { ok: true, result: `Edited ${path3.relative(this.projectDir,
|
|
1433
|
+
await fsp.writeFile(file2, updated, "utf8");
|
|
1434
|
+
return { ok: true, result: `Edited ${path3.relative(this.projectDir, file2)} (${count} replacement${count === 1 ? "" : "s"})` };
|
|
1420
1435
|
}
|
|
1421
1436
|
/**
|
|
1422
1437
|
* Copy a file from the THREAD filesystem (e.g. a generated /attachments/*
|
|
@@ -1839,11 +1854,11 @@ ${tail}` : " No output was captured.")
|
|
|
1839
1854
|
async walkGlob(base, pattern) {
|
|
1840
1855
|
const re = globToRegExp(pattern);
|
|
1841
1856
|
const out = [];
|
|
1842
|
-
const walk = async (
|
|
1843
|
-
const entries = await fsp.readdir(
|
|
1857
|
+
const walk = async (dir2) => {
|
|
1858
|
+
const entries = await fsp.readdir(dir2, { withFileTypes: true }).catch(() => []);
|
|
1844
1859
|
for (const e of entries) {
|
|
1845
1860
|
if (e.name === ".git" || e.name === "node_modules") continue;
|
|
1846
|
-
const full = path3.join(
|
|
1861
|
+
const full = path3.join(dir2, e.name);
|
|
1847
1862
|
if (e.isDirectory()) await walk(full);
|
|
1848
1863
|
else {
|
|
1849
1864
|
const rel = path3.relative(this.projectDir, full);
|
|
@@ -5039,11 +5054,11 @@ function relaxTlsForLocalEndpoint(endpoint) {
|
|
|
5039
5054
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
5040
5055
|
return true;
|
|
5041
5056
|
}
|
|
5042
|
-
var
|
|
5043
|
-
var
|
|
5057
|
+
var dir = () => path3.join(os9.homedir(), ".standardagents");
|
|
5058
|
+
var file = () => path3.join(dir(), "machine.json");
|
|
5044
5059
|
function loadMachineIdentity() {
|
|
5045
5060
|
try {
|
|
5046
|
-
const parsed = JSON.parse(fs4.readFileSync(
|
|
5061
|
+
const parsed = JSON.parse(fs4.readFileSync(file(), "utf8"));
|
|
5047
5062
|
if (typeof parsed.machine_id === "string" && parsed.machine_id.length > 0) {
|
|
5048
5063
|
return parsed;
|
|
5049
5064
|
}
|
|
@@ -5057,8 +5072,8 @@ function loadMachineIdentity() {
|
|
|
5057
5072
|
return identity;
|
|
5058
5073
|
}
|
|
5059
5074
|
function saveMachineIdentity(identity) {
|
|
5060
|
-
fs4.mkdirSync(
|
|
5061
|
-
fs4.writeFileSync(
|
|
5075
|
+
fs4.mkdirSync(dir(), { recursive: true });
|
|
5076
|
+
fs4.writeFileSync(file(), JSON.stringify(identity, null, 2), { mode: 384 });
|
|
5062
5077
|
}
|
|
5063
5078
|
function setMachineName(name) {
|
|
5064
5079
|
const identity = loadMachineIdentity();
|
|
@@ -5080,10 +5095,14 @@ function machineIdFromDaemonClientId(clientId) {
|
|
|
5080
5095
|
return clientId.startsWith("daemon:") ? clientId.slice("daemon:".length) : null;
|
|
5081
5096
|
}
|
|
5082
5097
|
var KEY_PREFIX = "standardcode.machine.";
|
|
5098
|
+
var CMD_SUFFIX = ".cmd";
|
|
5083
5099
|
var DAEMON_ONLINE_WINDOW_MS = 90 * 1e3;
|
|
5084
5100
|
function machineKey(machineId) {
|
|
5085
5101
|
return `${KEY_PREFIX}${machineId}`;
|
|
5086
5102
|
}
|
|
5103
|
+
function commandKey(machineId) {
|
|
5104
|
+
return `${KEY_PREFIX}${machineId}${CMD_SUFFIX}`;
|
|
5105
|
+
}
|
|
5087
5106
|
function parseMachineRecord(value) {
|
|
5088
5107
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
5089
5108
|
const r = value;
|
|
@@ -5094,6 +5113,7 @@ function parseMachineRecord(value) {
|
|
|
5094
5113
|
hostname: typeof r.hostname === "string" ? r.hostname : "",
|
|
5095
5114
|
platform: typeof r.platform === "string" ? r.platform : "",
|
|
5096
5115
|
arch: typeof r.arch === "string" ? r.arch : "",
|
|
5116
|
+
version: typeof r.version === "string" ? r.version : void 0,
|
|
5097
5117
|
daemon: r.daemon && typeof r.daemon === "object" && !Array.isArray(r.daemon) ? r.daemon : null,
|
|
5098
5118
|
projects: r.projects && typeof r.projects === "object" && !Array.isArray(r.projects) ? r.projects : {},
|
|
5099
5119
|
created_at: typeof r.created_at === "number" ? r.created_at : 0,
|
|
@@ -5118,6 +5138,7 @@ function newRecord(identity) {
|
|
|
5118
5138
|
hostname: os9.hostname(),
|
|
5119
5139
|
platform: process.platform,
|
|
5120
5140
|
arch: process.arch,
|
|
5141
|
+
version: readVersion() || void 0,
|
|
5121
5142
|
daemon: null,
|
|
5122
5143
|
projects: {},
|
|
5123
5144
|
created_at: now,
|
|
@@ -5127,10 +5148,11 @@ function newRecord(identity) {
|
|
|
5127
5148
|
async function updateOwnMachineRecord(api, identity, mutate) {
|
|
5128
5149
|
const existing = await loadMachine(api, identity.machine_id);
|
|
5129
5150
|
const record = existing ?? newRecord(identity);
|
|
5130
|
-
record.name = machineDisplayName(
|
|
5151
|
+
record.name = machineDisplayName(loadMachineIdentity());
|
|
5131
5152
|
record.hostname = os9.hostname();
|
|
5132
5153
|
record.platform = process.platform;
|
|
5133
5154
|
record.arch = process.arch;
|
|
5155
|
+
record.version = readVersion() || record.version;
|
|
5134
5156
|
mutate?.(record);
|
|
5135
5157
|
record.updated_at = Date.now();
|
|
5136
5158
|
await api.userKvSet(machineKey(identity.machine_id), record);
|
|
@@ -5179,6 +5201,63 @@ async function clearDaemon(api, identity) {
|
|
|
5179
5201
|
record.daemon = null;
|
|
5180
5202
|
});
|
|
5181
5203
|
}
|
|
5204
|
+
function parseCommands(value) {
|
|
5205
|
+
if (!Array.isArray(value)) return [];
|
|
5206
|
+
return value.filter(
|
|
5207
|
+
(c4) => !!c4 && typeof c4 === "object" && typeof c4.id === "string" && typeof c4.kind === "string"
|
|
5208
|
+
);
|
|
5209
|
+
}
|
|
5210
|
+
async function enqueueMachineCommand(api, machineId, kind, args) {
|
|
5211
|
+
const key = commandKey(machineId);
|
|
5212
|
+
const queue = parseCommands(await api.userKvGet(key));
|
|
5213
|
+
const cmd = {
|
|
5214
|
+
id: crypto.randomBytes(6).toString("hex"),
|
|
5215
|
+
kind,
|
|
5216
|
+
args,
|
|
5217
|
+
requested_at: Date.now()
|
|
5218
|
+
};
|
|
5219
|
+
queue.push(cmd);
|
|
5220
|
+
await api.userKvSet(key, queue);
|
|
5221
|
+
return cmd.id;
|
|
5222
|
+
}
|
|
5223
|
+
async function readMachineCommands(api, machineId) {
|
|
5224
|
+
return parseCommands(await api.userKvGet(commandKey(machineId)));
|
|
5225
|
+
}
|
|
5226
|
+
async function clearMachineCommands(api, machineId, appliedIds) {
|
|
5227
|
+
if (appliedIds.length === 0) return;
|
|
5228
|
+
const key = commandKey(machineId);
|
|
5229
|
+
const remaining = parseCommands(await api.userKvGet(key)).filter(
|
|
5230
|
+
(c4) => !appliedIds.includes(c4.id)
|
|
5231
|
+
);
|
|
5232
|
+
await api.userKvSet(key, remaining.length ? remaining : null);
|
|
5233
|
+
}
|
|
5234
|
+
async function applyMachineCommand(api, identity, cmd) {
|
|
5235
|
+
switch (cmd.kind) {
|
|
5236
|
+
case "rename": {
|
|
5237
|
+
const name = typeof cmd.args?.name === "string" ? cmd.args.name.trim() : "";
|
|
5238
|
+
if (!name) return "rename: ignored (empty name)";
|
|
5239
|
+
setMachineName(name);
|
|
5240
|
+
await updateOwnMachineRecord(api, identity);
|
|
5241
|
+
return `renamed to "${name}"`;
|
|
5242
|
+
}
|
|
5243
|
+
case "add_project": {
|
|
5244
|
+
const path12 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
|
|
5245
|
+
if (!path12) return "add_project: ignored (no path)";
|
|
5246
|
+
await registerProject(api, identity, path12);
|
|
5247
|
+
return `added project ${path12}`;
|
|
5248
|
+
}
|
|
5249
|
+
case "remove_project": {
|
|
5250
|
+
const path12 = typeof cmd.args?.path === "string" ? cmd.args.path : "";
|
|
5251
|
+
if (!path12) return "remove_project: ignored (no path)";
|
|
5252
|
+
await unregisterProject(api, identity, path12);
|
|
5253
|
+
return `removed project ${path12}`;
|
|
5254
|
+
}
|
|
5255
|
+
case "update":
|
|
5256
|
+
return "update requested";
|
|
5257
|
+
default:
|
|
5258
|
+
return `unknown command ${cmd.kind}`;
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5182
5261
|
|
|
5183
5262
|
// src/relay.ts
|
|
5184
5263
|
var APPROVAL_REQUEST_KEY = "approval_request";
|
|
@@ -5257,31 +5336,31 @@ function readCache() {
|
|
|
5257
5336
|
}
|
|
5258
5337
|
function writeCache(latest) {
|
|
5259
5338
|
try {
|
|
5260
|
-
const
|
|
5261
|
-
if (!fs4.existsSync(
|
|
5339
|
+
const dir2 = cacheDir();
|
|
5340
|
+
if (!fs4.existsSync(dir2)) fs4.mkdirSync(dir2, { recursive: true });
|
|
5262
5341
|
fs4.writeFileSync(cachePath(), JSON.stringify({ latest, timestamp: Date.now() }));
|
|
5263
5342
|
} catch {
|
|
5264
5343
|
}
|
|
5265
5344
|
}
|
|
5266
|
-
function readAutoUpdateState(
|
|
5345
|
+
function readAutoUpdateState(dir2 = cacheDir()) {
|
|
5267
5346
|
try {
|
|
5268
|
-
const raw = fs4.readFileSync(path3.join(
|
|
5347
|
+
const raw = fs4.readFileSync(path3.join(dir2, STATE_FILE), "utf-8");
|
|
5269
5348
|
const state = JSON.parse(raw);
|
|
5270
5349
|
return typeof state?.version === "string" ? state : null;
|
|
5271
5350
|
} catch {
|
|
5272
5351
|
return null;
|
|
5273
5352
|
}
|
|
5274
5353
|
}
|
|
5275
|
-
function writeAutoUpdateState(state,
|
|
5354
|
+
function writeAutoUpdateState(state, dir2 = cacheDir()) {
|
|
5276
5355
|
try {
|
|
5277
|
-
if (!fs4.existsSync(
|
|
5278
|
-
fs4.writeFileSync(path3.join(
|
|
5356
|
+
if (!fs4.existsSync(dir2)) fs4.mkdirSync(dir2, { recursive: true });
|
|
5357
|
+
fs4.writeFileSync(path3.join(dir2, STATE_FILE), JSON.stringify(state));
|
|
5279
5358
|
} catch {
|
|
5280
5359
|
}
|
|
5281
5360
|
}
|
|
5282
|
-
function clearAutoUpdateState(
|
|
5361
|
+
function clearAutoUpdateState(dir2 = cacheDir()) {
|
|
5283
5362
|
try {
|
|
5284
|
-
fs4.unlinkSync(path3.join(
|
|
5363
|
+
fs4.unlinkSync(path3.join(dir2, STATE_FILE));
|
|
5285
5364
|
} catch {
|
|
5286
5365
|
}
|
|
5287
5366
|
}
|
|
@@ -5321,10 +5400,10 @@ function decideAutoUpdate(info, opts) {
|
|
|
5321
5400
|
}
|
|
5322
5401
|
return "start";
|
|
5323
5402
|
}
|
|
5324
|
-
function startBackgroundUpdate(latest, pm,
|
|
5403
|
+
function startBackgroundUpdate(latest, pm, dir2 = cacheDir()) {
|
|
5325
5404
|
const startedAt = Date.now();
|
|
5326
|
-
writeAutoUpdateState({ version: latest, startedAt, exitCode: null },
|
|
5327
|
-
const stateFile = path3.join(
|
|
5405
|
+
writeAutoUpdateState({ version: latest, startedAt, exitCode: null }, dir2);
|
|
5406
|
+
const stateFile = path3.join(dir2, STATE_FILE);
|
|
5328
5407
|
const { cmd, args } = updateCommand(pm);
|
|
5329
5408
|
const script = `const cp=require('child_process');const fs=require('fs');const r=cp.spawnSync(${JSON.stringify(cmd)},${JSON.stringify(args)},{shell:process.platform==='win32',encoding:'utf8'});const out=((r.stdout||'')+(r.stderr||'')).slice(-2000);fs.writeFileSync(${JSON.stringify(stateFile)},JSON.stringify({version:${JSON.stringify(latest)},startedAt:${startedAt},exitCode:r.status==null?-1:r.status,finishedAt:Date.now(),output:out}));`;
|
|
5330
5409
|
try {
|
|
@@ -5332,14 +5411,14 @@ function startBackgroundUpdate(latest, pm, dir = cacheDir()) {
|
|
|
5332
5411
|
child.unref();
|
|
5333
5412
|
return true;
|
|
5334
5413
|
} catch {
|
|
5335
|
-
writeAutoUpdateState({ version: latest, startedAt, exitCode: -1, finishedAt: Date.now() },
|
|
5414
|
+
writeAutoUpdateState({ version: latest, startedAt, exitCode: -1, finishedAt: Date.now() }, dir2);
|
|
5336
5415
|
return false;
|
|
5337
5416
|
}
|
|
5338
5417
|
}
|
|
5339
|
-
function consumeAppliedUpdate(currentVersion,
|
|
5340
|
-
const state = readAutoUpdateState(
|
|
5418
|
+
function consumeAppliedUpdate(currentVersion, dir2 = cacheDir()) {
|
|
5419
|
+
const state = readAutoUpdateState(dir2);
|
|
5341
5420
|
if (!state || state.version !== currentVersion) return null;
|
|
5342
|
-
clearAutoUpdateState(
|
|
5421
|
+
clearAutoUpdateState(dir2);
|
|
5343
5422
|
return state.exitCode === 0 ? state : null;
|
|
5344
5423
|
}
|
|
5345
5424
|
async function fetchLatest(currentVersion) {
|
|
@@ -5412,6 +5491,7 @@ function runUpdate(pm) {
|
|
|
5412
5491
|
|
|
5413
5492
|
// src/daemon.ts
|
|
5414
5493
|
var HEARTBEAT_MS = 3e4;
|
|
5494
|
+
var COMMAND_POLL_MS = 8e3;
|
|
5415
5495
|
var RECLAIM_PROBE_MS = 2 * 6e4;
|
|
5416
5496
|
var UPDATE_CHECK_MS = 6 * 60 * 6e4;
|
|
5417
5497
|
var SWEEP_MS = 10 * 6e4;
|
|
@@ -5674,6 +5754,55 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5674
5754
|
};
|
|
5675
5755
|
void checkUpdates();
|
|
5676
5756
|
const updateTimer = setInterval(() => void checkUpdates(), UPDATE_CHECK_MS);
|
|
5757
|
+
let draining = false;
|
|
5758
|
+
const drainCommands = async () => {
|
|
5759
|
+
if (draining) return;
|
|
5760
|
+
draining = true;
|
|
5761
|
+
try {
|
|
5762
|
+
const cmds = await readMachineCommands(api, identity.machine_id);
|
|
5763
|
+
if (cmds.length === 0) return;
|
|
5764
|
+
const applied2 = [];
|
|
5765
|
+
let forcedUpdate = false;
|
|
5766
|
+
for (const cmd of cmds) {
|
|
5767
|
+
try {
|
|
5768
|
+
if (cmd.kind === "update") {
|
|
5769
|
+
forcedUpdate = true;
|
|
5770
|
+
applied2.push(cmd.id);
|
|
5771
|
+
continue;
|
|
5772
|
+
}
|
|
5773
|
+
const result = await applyMachineCommand(api, identity, cmd);
|
|
5774
|
+
daemonLog(`command ${cmd.kind}: ${result}`);
|
|
5775
|
+
applied2.push(cmd.id);
|
|
5776
|
+
} catch (e) {
|
|
5777
|
+
daemonLog(`command ${cmd.kind} failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
5778
|
+
applied2.push(cmd.id);
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
await clearMachineCommands(api, identity.machine_id, applied2).catch(() => {
|
|
5782
|
+
});
|
|
5783
|
+
if (forcedUpdate) {
|
|
5784
|
+
const pm = detectPackageManager();
|
|
5785
|
+
if (!pm) {
|
|
5786
|
+
daemonLog("forced update requested but this is not an installed build \u2014 ignoring");
|
|
5787
|
+
} else if (![...workers.values()].some((w) => w.busy)) {
|
|
5788
|
+
daemonLog(`forced update: running ${pm} install now`);
|
|
5789
|
+
const { ok, output: output4 } = await runUpdate(pm);
|
|
5790
|
+
daemonLog(`forced update ${ok ? "succeeded" : "failed"}: ${output4.trim().split("\n").slice(-2).join(" | ")}`);
|
|
5791
|
+
if (ok) {
|
|
5792
|
+
daemonLog("restarting to apply the forced update");
|
|
5793
|
+
shutdown(0);
|
|
5794
|
+
}
|
|
5795
|
+
} else {
|
|
5796
|
+
daemonLog("forced update deferred \u2014 a session is busy; will retry");
|
|
5797
|
+
void checkUpdates();
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
} finally {
|
|
5801
|
+
draining = false;
|
|
5802
|
+
}
|
|
5803
|
+
};
|
|
5804
|
+
void drainCommands();
|
|
5805
|
+
const commandTimer = setInterval(() => void drainCommands(), COMMAND_POLL_MS);
|
|
5677
5806
|
const sweeper = setInterval(() => {
|
|
5678
5807
|
if (updateReady && ![...workers.values()].some((w) => w.busy)) {
|
|
5679
5808
|
daemonLog("restarting to apply the installed update");
|
|
@@ -5686,6 +5815,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
|
|
|
5686
5815
|
clearInterval(heartbeat);
|
|
5687
5816
|
clearInterval(reclaim);
|
|
5688
5817
|
clearInterval(updateTimer);
|
|
5818
|
+
clearInterval(commandTimer);
|
|
5689
5819
|
clearInterval(sweeper);
|
|
5690
5820
|
for (const worker of workers.values()) worker.stop();
|
|
5691
5821
|
events.close();
|
|
@@ -5705,15 +5835,15 @@ function resolveDaemonCommand(extraArgs = []) {
|
|
|
5705
5835
|
if (!entry) throw new Error("Cannot determine how this CLI was launched.");
|
|
5706
5836
|
const argv = [process.execPath];
|
|
5707
5837
|
if (entry.endsWith(".ts")) {
|
|
5708
|
-
let
|
|
5838
|
+
let dir2 = path3.dirname(entry);
|
|
5709
5839
|
let tsx = null;
|
|
5710
|
-
for (let i = 0; i < 6 &&
|
|
5711
|
-
const candidate = path3.join(
|
|
5840
|
+
for (let i = 0; i < 6 && dir2 !== path3.dirname(dir2); i++) {
|
|
5841
|
+
const candidate = path3.join(dir2, "node_modules", "tsx", "dist", "cli.mjs");
|
|
5712
5842
|
if (fs4.existsSync(candidate)) {
|
|
5713
5843
|
tsx = candidate;
|
|
5714
5844
|
break;
|
|
5715
5845
|
}
|
|
5716
|
-
|
|
5846
|
+
dir2 = path3.dirname(dir2);
|
|
5717
5847
|
}
|
|
5718
5848
|
if (!tsx) {
|
|
5719
5849
|
throw new Error(
|
|
@@ -6048,9 +6178,9 @@ async function projectCommand(action, target) {
|
|
|
6048
6178
|
`);
|
|
6049
6179
|
process.exit(1);
|
|
6050
6180
|
}
|
|
6051
|
-
const
|
|
6052
|
-
if (action === "add" && !fs4.existsSync(
|
|
6053
|
-
stdout.write(`${c2.red}\u2717${c2.reset} ${
|
|
6181
|
+
const dir2 = path3.resolve(target);
|
|
6182
|
+
if (action === "add" && !fs4.existsSync(dir2)) {
|
|
6183
|
+
stdout.write(`${c2.red}\u2717${c2.reset} ${dir2} does not exist on this machine.
|
|
6054
6184
|
`);
|
|
6055
6185
|
process.exit(1);
|
|
6056
6186
|
}
|
|
@@ -6058,12 +6188,12 @@ async function projectCommand(action, target) {
|
|
|
6058
6188
|
const api = await ensureSignedIn(endpoint);
|
|
6059
6189
|
const identity = loadMachineIdentity();
|
|
6060
6190
|
if (action === "add") {
|
|
6061
|
-
await registerProject(api, identity,
|
|
6062
|
-
stdout.write(`${c2.green}\u2713${c2.reset} Registered ${
|
|
6191
|
+
await registerProject(api, identity, dir2);
|
|
6192
|
+
stdout.write(`${c2.green}\u2713${c2.reset} Registered ${dir2} for remote sessions on ${machineDisplayName(identity)}.
|
|
6063
6193
|
`);
|
|
6064
6194
|
} else {
|
|
6065
|
-
await unregisterProject(api, identity,
|
|
6066
|
-
stdout.write(`${c2.green}\u2713${c2.reset} Removed ${
|
|
6195
|
+
await unregisterProject(api, identity, dir2);
|
|
6196
|
+
stdout.write(`${c2.green}\u2713${c2.reset} Removed ${dir2} from this machine's projects.
|
|
6067
6197
|
`);
|
|
6068
6198
|
}
|
|
6069
6199
|
}
|
|
@@ -6195,6 +6325,20 @@ function parseArgs2(args) {
|
|
|
6195
6325
|
}
|
|
6196
6326
|
return parsed;
|
|
6197
6327
|
}
|
|
6328
|
+
function printCommandBlock(tui, command, output4, ok, where) {
|
|
6329
|
+
tui.print("");
|
|
6330
|
+
const note = where ? ` ${c3.dim}(ran on ${where})${c3.reset}` : "";
|
|
6331
|
+
tui.print(`${c3.magenta}!${c3.reset} ${c3.bold}${command}${c3.reset}${note}`);
|
|
6332
|
+
const body = (output4 ?? "").replace(/\s+$/, "");
|
|
6333
|
+
if (body) {
|
|
6334
|
+
for (const line of body.split("\n")) {
|
|
6335
|
+
tui.print(` ${ok ? c3.dim : c3.red}${line}${c3.reset}`);
|
|
6336
|
+
}
|
|
6337
|
+
} else {
|
|
6338
|
+
tui.print(` ${c3.dim}(no output)${c3.reset}`);
|
|
6339
|
+
}
|
|
6340
|
+
tui.print("");
|
|
6341
|
+
}
|
|
6198
6342
|
function printAssistant(tui, text) {
|
|
6199
6343
|
const cols2 = Math.max(20, (process.stdout.columns || 80) - 3);
|
|
6200
6344
|
tui.clearStream();
|
|
@@ -6243,7 +6387,7 @@ ${c3.teal}\u25C7${c3.reset} ${c3.dim}Standard Code \u2014 see you soon.${c3.rese
|
|
|
6243
6387
|
}
|
|
6244
6388
|
function printWelcome(endpoint, projectDir) {
|
|
6245
6389
|
const home = os9.homedir();
|
|
6246
|
-
const
|
|
6390
|
+
const dir2 = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
|
|
6247
6391
|
const host = endpoint.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
6248
6392
|
const version = readVersion();
|
|
6249
6393
|
const pad = " ";
|
|
@@ -6251,7 +6395,7 @@ function printWelcome(endpoint, projectDir) {
|
|
|
6251
6395
|
`${c3.bold}${gradientText("Standard Code")}${c3.reset}${version ? ` ${c3.dim}v${version}${c3.reset}` : ""}`,
|
|
6252
6396
|
`${c3.dim}terminal coding agent${c3.reset}`,
|
|
6253
6397
|
...endpoint === PRODUCTION_ENDPOINT ? [] : [`${c3.teal}${host}${c3.reset}`],
|
|
6254
|
-
`${c3.dim}${
|
|
6398
|
+
`${c3.dim}${dir2}${c3.reset}`
|
|
6255
6399
|
];
|
|
6256
6400
|
const markWidth = Math.max(...LOGO_MARK.map((l) => [...l].length));
|
|
6257
6401
|
const metaTop = Math.floor((LOGO_MARK.length - meta.length) / 2);
|
|
@@ -6598,10 +6742,10 @@ async function pickRemoteProject(tui, runner) {
|
|
|
6598
6742
|
const projects = Object.entries(runner.projects).sort(
|
|
6599
6743
|
(a, b) => (b[1]?.last_used_at ?? 0) - (a[1]?.last_used_at ?? 0)
|
|
6600
6744
|
);
|
|
6601
|
-
const items = projects.map(([
|
|
6602
|
-
label: shortenPath(
|
|
6745
|
+
const items = projects.map(([dir2, p]) => ({
|
|
6746
|
+
label: shortenPath(dir2, 48),
|
|
6603
6747
|
hint: p?.last_used_at ? relativeTime(p.last_used_at / 1e3) : "",
|
|
6604
|
-
value:
|
|
6748
|
+
value: dir2
|
|
6605
6749
|
}));
|
|
6606
6750
|
items.push({ label: `\uFF0B Another path on ${runner.name}\u2026`, hint: "type a directory", value: ENTER_PATH });
|
|
6607
6751
|
const picked = await tui.select(
|
|
@@ -6687,12 +6831,23 @@ async function printHistory(api, threadId, tui) {
|
|
|
6687
6831
|
} catch {
|
|
6688
6832
|
return;
|
|
6689
6833
|
}
|
|
6690
|
-
const convo = msgs.filter(
|
|
6834
|
+
const convo = msgs.filter(
|
|
6835
|
+
(m) => m.metadata?.user_command || m.role === "user" || m.role === "assistant" && messageText(m.content).trim()
|
|
6836
|
+
).sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
|
|
6691
6837
|
if (!convo.length) return;
|
|
6692
6838
|
const shown = convo.slice(-24);
|
|
6693
6839
|
tui.print(`${c3.dim}\u2500\u2500 resuming session \xB7 ${convo.length} message${convo.length === 1 ? "" : "s"} \u2500\u2500${c3.reset}`);
|
|
6694
6840
|
if (shown.length < convo.length) tui.print(`${c3.dim} \u2026 earlier messages omitted${c3.reset}`);
|
|
6695
6841
|
for (const m of shown) {
|
|
6842
|
+
if (m.metadata?.user_command) {
|
|
6843
|
+
printCommandBlock(
|
|
6844
|
+
tui,
|
|
6845
|
+
String(m.metadata.command ?? ""),
|
|
6846
|
+
String(m.metadata.output ?? messageText(m.content)),
|
|
6847
|
+
m.metadata.ok !== false
|
|
6848
|
+
);
|
|
6849
|
+
continue;
|
|
6850
|
+
}
|
|
6696
6851
|
const text = messageText(m.content).trim();
|
|
6697
6852
|
if (!text) continue;
|
|
6698
6853
|
if (m.role === "user") tui.printUserMessage(text);
|
|
@@ -7009,6 +7164,25 @@ why: ${req.requestPermission}` : ""}`,
|
|
|
7009
7164
|
busy = true;
|
|
7010
7165
|
tui.setWorking(true);
|
|
7011
7166
|
};
|
|
7167
|
+
const whereLabel = remote ? runnerName : "this machine";
|
|
7168
|
+
let bangRunning = false;
|
|
7169
|
+
const runBangCommand = async (command) => {
|
|
7170
|
+
if (bangRunning) {
|
|
7171
|
+
tui.print(`${c3.dim}a command is already running \u2014 one at a time.${c3.reset}`);
|
|
7172
|
+
return;
|
|
7173
|
+
}
|
|
7174
|
+
bangRunning = true;
|
|
7175
|
+
tui.print(`${c3.magenta}!${c3.reset} ${c3.dim}running on ${whereLabel}\u2026${c3.reset}`);
|
|
7176
|
+
try {
|
|
7177
|
+
const res = await api.runCommand(threadId, command);
|
|
7178
|
+
if (res.messageId) shownIds.add(res.messageId);
|
|
7179
|
+
printCommandBlock(tui, command, res.ok ? res.output ?? "" : res.error ?? "command failed", res.ok, whereLabel);
|
|
7180
|
+
} catch (e) {
|
|
7181
|
+
printCommandBlock(tui, command, e instanceof Error ? e.message : String(e), false, whereLabel);
|
|
7182
|
+
} finally {
|
|
7183
|
+
bangRunning = false;
|
|
7184
|
+
}
|
|
7185
|
+
};
|
|
7012
7186
|
const flushQueued = async () => {
|
|
7013
7187
|
if (!queued.length) return;
|
|
7014
7188
|
const toSend = queued.splice(0);
|
|
@@ -7173,6 +7347,12 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7173
7347
|
run: () => runMcpMenu(tui, mcpCtl)
|
|
7174
7348
|
}
|
|
7175
7349
|
],
|
|
7350
|
+
{
|
|
7351
|
+
name: "machines",
|
|
7352
|
+
label: "Your machines",
|
|
7353
|
+
hint: "list, rename, update, manage projects",
|
|
7354
|
+
run: () => runMachinesMenu(tui, api, session.identity)
|
|
7355
|
+
},
|
|
7176
7356
|
{
|
|
7177
7357
|
name: "daemon",
|
|
7178
7358
|
label: "Machine daemon",
|
|
@@ -7206,7 +7386,18 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
|
|
|
7206
7386
|
const history = await loadHistory(api, threadId, historySeedThreadId);
|
|
7207
7387
|
tui.setHistory(history);
|
|
7208
7388
|
tui.onSubmit = (text, images) => {
|
|
7209
|
-
|
|
7389
|
+
const trimmed = text.trimStart();
|
|
7390
|
+
if (trimmed.startsWith("!") && !trimmed.startsWith("!!")) {
|
|
7391
|
+
const command = trimmed.slice(1).trim();
|
|
7392
|
+
if (command) {
|
|
7393
|
+
appendHistory(api, threadId, history, text);
|
|
7394
|
+
void runBangCommand(command);
|
|
7395
|
+
}
|
|
7396
|
+
return;
|
|
7397
|
+
}
|
|
7398
|
+
const outgoing = trimmed.startsWith("!!") ? text.replace("!!", "!") : text;
|
|
7399
|
+
appendHistory(api, threadId, history, outgoing);
|
|
7400
|
+
text = outgoing;
|
|
7210
7401
|
if (editingQueued) {
|
|
7211
7402
|
editingQueued = false;
|
|
7212
7403
|
queued.push({ text, images });
|
|
@@ -7339,6 +7530,15 @@ ${c3.dim}runs on ${request.machine || runnerName}${c3.reset}`,
|
|
|
7339
7530
|
void offerUpgrade({ auto: true });
|
|
7340
7531
|
continue;
|
|
7341
7532
|
}
|
|
7533
|
+
if (m.metadata?.user_command) {
|
|
7534
|
+
printCommandBlock(
|
|
7535
|
+
tui,
|
|
7536
|
+
String(m.metadata.command ?? ""),
|
|
7537
|
+
String(m.metadata.output ?? text),
|
|
7538
|
+
m.metadata.ok !== false
|
|
7539
|
+
);
|
|
7540
|
+
continue;
|
|
7541
|
+
}
|
|
7342
7542
|
if (m.role === "assistant" && text) printAssistant(tui, text);
|
|
7343
7543
|
else if (m.role === "system" && text) tui.print(`${c3.dim}${text}${c3.reset}`);
|
|
7344
7544
|
else if (m.role === "user" && text) {
|
|
@@ -7492,6 +7692,117 @@ async function runLevelMenu(tui, perm) {
|
|
|
7492
7692
|
perm.level = picked;
|
|
7493
7693
|
}
|
|
7494
7694
|
}
|
|
7695
|
+
async function runMachinesMenu(tui, api, self) {
|
|
7696
|
+
let machines;
|
|
7697
|
+
try {
|
|
7698
|
+
machines = await loadMachines(api);
|
|
7699
|
+
} catch (e) {
|
|
7700
|
+
tui.print(`${c3.red}\u2717 couldn't load machines:${c3.reset} ${c3.gray}${e instanceof Error ? e.message : String(e)}${c3.reset}`);
|
|
7701
|
+
return;
|
|
7702
|
+
}
|
|
7703
|
+
if (!machines.length) {
|
|
7704
|
+
tui.print(`${c3.gray}No machines registered yet. Run standardcode on a machine (or install its daemon) to register it.${c3.reset}`);
|
|
7705
|
+
return;
|
|
7706
|
+
}
|
|
7707
|
+
machines.sort((a, b) => (b.updated_at ?? 0) - (a.updated_at ?? 0));
|
|
7708
|
+
const picked = await tui.select(
|
|
7709
|
+
`${c3.bold}Your machines${c3.reset} ${c3.dim}(\u2191/\u2193 \xB7 enter \xB7 esc)${c3.reset}`,
|
|
7710
|
+
machines.map((m) => {
|
|
7711
|
+
const isSelf = m.id === self.machine_id;
|
|
7712
|
+
const online = daemonOnline(m);
|
|
7713
|
+
const daemonBit = m.daemon ? online ? "daemon online" : "daemon offline" : "no daemon";
|
|
7714
|
+
const nproj = Object.keys(m.projects).length;
|
|
7715
|
+
return {
|
|
7716
|
+
label: `${m.name}${isSelf ? " (this machine)" : ""}`,
|
|
7717
|
+
hint: `${m.hostname} \xB7 ${m.platform}/${m.arch} \xB7 v${m.version ?? "?"} \xB7 ${daemonBit} \xB7 ${nproj} project${nproj === 1 ? "" : "s"}`,
|
|
7718
|
+
value: m.id
|
|
7719
|
+
};
|
|
7720
|
+
})
|
|
7721
|
+
);
|
|
7722
|
+
if (!picked) return;
|
|
7723
|
+
const machine = machines.find((m) => m.id === picked);
|
|
7724
|
+
await manageMachine(tui, api, self, machine);
|
|
7725
|
+
}
|
|
7726
|
+
async function manageMachine(tui, api, self, machine) {
|
|
7727
|
+
const isSelf = machine.id === self.machine_id;
|
|
7728
|
+
const online = daemonOnline(machine);
|
|
7729
|
+
const remoteReachable = !isSelf && !!machine.daemon;
|
|
7730
|
+
if (!isSelf && !machine.daemon) {
|
|
7731
|
+
tui.print(
|
|
7732
|
+
`${c3.yellow}${machine.name} has no daemon${c3.reset} ${c3.gray}\u2014 it only runs the interactive CLI, so it can't be managed remotely. Manage it from that machine, or install its daemon.${c3.reset}`
|
|
7733
|
+
);
|
|
7734
|
+
return;
|
|
7735
|
+
}
|
|
7736
|
+
if (remoteReachable && !online) {
|
|
7737
|
+
tui.print(
|
|
7738
|
+
`${c3.dim}${machine.name}'s daemon is offline \u2014 queued changes apply when it next comes online.${c3.reset}`
|
|
7739
|
+
);
|
|
7740
|
+
}
|
|
7741
|
+
const action = await tui.select(`${c3.bold}${machine.name}${c3.reset}`, [
|
|
7742
|
+
{ label: "Rename", value: "rename" },
|
|
7743
|
+
{ label: "Update standardcode", hint: `v${machine.version ?? "?"} \u2192 latest`, value: "update" },
|
|
7744
|
+
{ label: "Manage projects", hint: `${Object.keys(machine.projects).length}`, value: "projects" },
|
|
7745
|
+
{ label: "Back", value: "back" }
|
|
7746
|
+
]);
|
|
7747
|
+
if (!action || action === "back") return;
|
|
7748
|
+
const dispatch = async (kind, args) => {
|
|
7749
|
+
if (isSelf) {
|
|
7750
|
+
await applyMachineCommand(api, self, { kind, args});
|
|
7751
|
+
} else {
|
|
7752
|
+
await enqueueMachineCommand(api, machine.id, kind, args);
|
|
7753
|
+
}
|
|
7754
|
+
};
|
|
7755
|
+
const applyNote = isSelf ? "applied" : `queued \u2014 ${machine.name}'s daemon will apply it within ~10s`;
|
|
7756
|
+
if (action === "rename") {
|
|
7757
|
+
const name = await tui.prompt(`New name for ${machine.name}`, machine.name);
|
|
7758
|
+
if (!name || !name.trim()) return;
|
|
7759
|
+
await dispatch("rename", { name: name.trim() });
|
|
7760
|
+
tui.print(`${c3.green}\u2713${c3.reset} Rename to "${name.trim()}" ${c3.gray}${applyNote}.${c3.reset}`);
|
|
7761
|
+
} else if (action === "update") {
|
|
7762
|
+
if (isSelf) {
|
|
7763
|
+
await runUpdateCommand(tui);
|
|
7764
|
+
} else {
|
|
7765
|
+
const go = await tui.select(`Force ${machine.name} to update standardcode now?`, [
|
|
7766
|
+
{ label: "Yes, update & restart its daemon", value: "yes" },
|
|
7767
|
+
{ label: "Cancel", value: "no" }
|
|
7768
|
+
]);
|
|
7769
|
+
if (go !== "yes") return;
|
|
7770
|
+
await dispatch("update");
|
|
7771
|
+
tui.print(`${c3.green}\u2713${c3.reset} Update ${c3.gray}${applyNote} (its daemon updates and restarts on the new version).${c3.reset}`);
|
|
7772
|
+
}
|
|
7773
|
+
} else if (action === "projects") {
|
|
7774
|
+
await manageMachineProjects(tui, api, self, machine, dispatch, applyNote);
|
|
7775
|
+
}
|
|
7776
|
+
}
|
|
7777
|
+
async function manageMachineProjects(tui, api, self, machine, dispatch, applyNote) {
|
|
7778
|
+
const ADD = "__add__";
|
|
7779
|
+
const paths = Object.keys(machine.projects).sort();
|
|
7780
|
+
const picked = await tui.select(
|
|
7781
|
+
`${c3.bold}Projects on ${machine.name}${c3.reset} ${c3.dim}(enter to remove \xB7 esc)${c3.reset}`,
|
|
7782
|
+
[
|
|
7783
|
+
...paths.map((p) => ({ label: p, hint: "enter to remove", value: p })),
|
|
7784
|
+
{ label: "\uFF0B Add a project directory\u2026", hint: "absolute path", value: ADD }
|
|
7785
|
+
]
|
|
7786
|
+
);
|
|
7787
|
+
if (!picked) return;
|
|
7788
|
+
if (picked === ADD) {
|
|
7789
|
+
const path12 = await tui.prompt(
|
|
7790
|
+
`Absolute project path on ${machine.name}`,
|
|
7791
|
+
machine.id === self.machine_id ? process.cwd() : "/home/you/project"
|
|
7792
|
+
);
|
|
7793
|
+
if (!path12 || !path12.trim()) return;
|
|
7794
|
+
const trimmed = path12.trim();
|
|
7795
|
+
if (!trimmed.startsWith("/") && !trimmed.startsWith("~")) {
|
|
7796
|
+
tui.print(`${c3.yellow}Use an absolute path (starting with / or ~).${c3.reset}`);
|
|
7797
|
+
return;
|
|
7798
|
+
}
|
|
7799
|
+
await dispatch("add_project", { path: trimmed });
|
|
7800
|
+
tui.print(`${c3.green}\u2713${c3.reset} Add ${trimmed} ${c3.gray}${applyNote}.${c3.reset}`);
|
|
7801
|
+
} else {
|
|
7802
|
+
await dispatch("remove_project", { path: picked });
|
|
7803
|
+
tui.print(`${c3.green}\u2713${c3.reset} Remove ${picked} ${c3.gray}${applyNote}.${c3.reset}`);
|
|
7804
|
+
}
|
|
7805
|
+
}
|
|
7495
7806
|
function showDaemonInfo(tui, session) {
|
|
7496
7807
|
if (session.mode === "remote" && session.runner) {
|
|
7497
7808
|
tui.print(
|
|
@@ -7519,6 +7830,7 @@ function showKeybindings(tui) {
|
|
|
7519
7830
|
tui.print(`${c3.gray}shortcuts:${c3.reset}`);
|
|
7520
7831
|
tui.print(`${c3.gray} shift-tab${c3.reset} cycle auto-accept level (1\u20135)`);
|
|
7521
7832
|
tui.print(`${c3.gray} /${c3.reset} open the command palette (type to filter)`);
|
|
7833
|
+
tui.print(`${c3.gray} !cmd${c3.reset} run a shell command on the session's machine (e.g. !ls); !! to send a literal !`);
|
|
7522
7834
|
tui.print(`${c3.gray} ctrl-v${c3.reset} paste an image from the clipboard ([#Image 1])`);
|
|
7523
7835
|
tui.print(`${c3.gray} \u2191 / \u2193${c3.reset} cycle past messages (on the input's top line)`);
|
|
7524
7836
|
tui.print(`${c3.gray} \u2190${c3.reset} from the start of the input: select the [\u2699 n bg] badge (enter opens it)`);
|