@tamsensedev/runner 0.18.0 → 0.20.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/README.md +15 -0
- package/dist/cli.js +149 -84
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,21 @@ Options:
|
|
|
50
50
|
- `--server <url>` TAMsense address, default https://my.tamsense.com (login)
|
|
51
51
|
- `--parallel <n>` how many tasks to work on at once (start)
|
|
52
52
|
- `--no-browser` do not install the headless browser for interface checks (start)
|
|
53
|
+
- `-p <name>`, also `--p` or `--profile`: which connection to use (login, start, status, logout)
|
|
54
|
+
|
|
55
|
+
## Several projects on one computer
|
|
56
|
+
|
|
57
|
+
One connection works for one account in one project. For another project, or another account, use a profile: a separate connection with its own name.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
cd C:\work\client-a
|
|
61
|
+
npx @tamsensedev/runner@latest login -p client-a
|
|
62
|
+
npx @tamsensedev/runner@latest start -p client-a
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Confirm the code in the browser while the project of that profile is open in TAMsense. Each profile keeps its own login, folders and task copies in `~/.tamsense/profiles/<name>`, and its computer is called `<computer name> (<profile>)` on the site. Run each profile in its own terminal, at the same time if you like. Claude Code and the headless browser are shared. Without `-p` the program uses the main connection, as before.
|
|
66
|
+
|
|
67
|
+
A profile name may have letters, digits, `-` and `_`, up to 32 characters, and is case-insensitive.
|
|
53
68
|
|
|
54
69
|
## What it does on your computer
|
|
55
70
|
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { existsSync as existsSync8 } from "fs";
|
|
5
|
-
import { homedir as
|
|
5
|
+
import { homedir as homedir2 } from "os";
|
|
6
6
|
import process8 from "process";
|
|
7
7
|
|
|
8
8
|
// src/api.ts
|
|
@@ -252,6 +252,7 @@ var START_COMMAND = "npx @tamsensedev/runner@latest start";
|
|
|
252
252
|
var BROWSER_COMMAND = "npx @tamsensedev/runner@latest browser";
|
|
253
253
|
var DEFAULT_PARALLEL = 2;
|
|
254
254
|
var MAX_PARALLEL = 4;
|
|
255
|
+
var PROFILE_NAME = /^[a-z0-9][\w-]{0,31}$/i;
|
|
255
256
|
var HELP_TEXT = `Connects this computer to TAMsense so it can work on your tasks.
|
|
256
257
|
|
|
257
258
|
Usage:
|
|
@@ -263,6 +264,12 @@ Usage:
|
|
|
263
264
|
npx @tamsensedev/runner@latest status show the connection
|
|
264
265
|
npx @tamsensedev/runner@latest logout disconnect this computer
|
|
265
266
|
|
|
267
|
+
Profiles, for several projects or accounts on one computer:
|
|
268
|
+
-p, --profile <name> use a separate connection with its own folders, for example:
|
|
269
|
+
${LOGIN_COMMAND} -p client-a
|
|
270
|
+
${START_COMMAND} -p client-a
|
|
271
|
+
Works with login, start, status and logout. Without it, the main connection is used.
|
|
272
|
+
|
|
266
273
|
Login options:
|
|
267
274
|
--name <name> how this computer is called in TAMsense (default: its network name)
|
|
268
275
|
--server <url> TAMsense address (default: ${DEFAULT_SERVER})
|
|
@@ -270,6 +277,18 @@ Login options:
|
|
|
270
277
|
Start options:
|
|
271
278
|
--parallel <n> how many tasks to work on at once, 1 to ${MAX_PARALLEL} (default: ${DEFAULT_PARALLEL}, or TAMSENSE_PARALLEL)
|
|
272
279
|
--no-browser do not install the headless browser for interface checks (start installs it when it is missing)`;
|
|
280
|
+
function profileFor(flag) {
|
|
281
|
+
if (flag === void 0) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (!PROFILE_NAME.test(flag)) {
|
|
285
|
+
throw new Error(`The profile name may have letters, digits, - and _, up to 32 characters, got "${flag}".`);
|
|
286
|
+
}
|
|
287
|
+
return flag.toLowerCase();
|
|
288
|
+
}
|
|
289
|
+
function withProfile(command, profile) {
|
|
290
|
+
return profile === null ? command : `${command} -p ${profile}`;
|
|
291
|
+
}
|
|
273
292
|
function parallelFor(flag, env) {
|
|
274
293
|
const [source, value] = flag !== void 0 ? ["--parallel", flag] : ["TAMSENSE_PARALLEL", env?.trim() || void 0];
|
|
275
294
|
if (value === void 0) {
|
|
@@ -287,6 +306,8 @@ function parseCommand(argv, env = {}) {
|
|
|
287
306
|
allowPositionals: true,
|
|
288
307
|
strict: true,
|
|
289
308
|
options: {
|
|
309
|
+
"profile": { type: "string", short: "p" },
|
|
310
|
+
"p": { type: "string" },
|
|
290
311
|
"server": { type: "string" },
|
|
291
312
|
"name": { type: "string" },
|
|
292
313
|
"parallel": { type: "string" },
|
|
@@ -304,14 +325,15 @@ function parseCommand(argv, env = {}) {
|
|
|
304
325
|
if (positionals.length !== 1) {
|
|
305
326
|
return { name: "help" };
|
|
306
327
|
}
|
|
328
|
+
const profile = profileFor(values.profile ?? values.p);
|
|
307
329
|
if (name === "login") {
|
|
308
|
-
return { name: "login", server: values.server ?? null, deviceName: values.name ?? null };
|
|
330
|
+
return { name: "login", profile, server: values.server ?? null, deviceName: values.name ?? null };
|
|
309
331
|
}
|
|
310
332
|
if (name === "start") {
|
|
311
|
-
return { name: "start", parallel: parallelFor(values.parallel, env.TAMSENSE_PARALLEL), browser: !values["no-browser"] };
|
|
333
|
+
return { name: "start", profile, parallel: parallelFor(values.parallel, env.TAMSENSE_PARALLEL), browser: !values["no-browser"] };
|
|
312
334
|
}
|
|
313
335
|
if (name === "status" || name === "logout") {
|
|
314
|
-
return { name };
|
|
336
|
+
return { name, profile };
|
|
315
337
|
}
|
|
316
338
|
return { name: "help" };
|
|
317
339
|
}
|
|
@@ -1554,8 +1576,8 @@ var cacheLock = keyedLock();
|
|
|
1554
1576
|
function githubRemoteUrl(fullName) {
|
|
1555
1577
|
return `https://github.com/${fullName}.git`;
|
|
1556
1578
|
}
|
|
1557
|
-
function tamsenseFolder(
|
|
1558
|
-
return join3(
|
|
1579
|
+
function tamsenseFolder(dataFolder, ...parts) {
|
|
1580
|
+
return join3(dataFolder, ...parts);
|
|
1559
1581
|
}
|
|
1560
1582
|
function folderNames(repositories) {
|
|
1561
1583
|
const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
|
|
@@ -1619,15 +1641,15 @@ async function keptBase(cache, path, job, repository) {
|
|
|
1619
1641
|
}
|
|
1620
1642
|
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
1621
1643
|
}
|
|
1622
|
-
function taskFolder(
|
|
1623
|
-
return tamsenseFolder(
|
|
1644
|
+
function taskFolder(dataFolder, job) {
|
|
1645
|
+
return tamsenseFolder(dataFolder, "work", ...stepFolder(job));
|
|
1624
1646
|
}
|
|
1625
1647
|
async function prepareWorkspace(job, token, tools) {
|
|
1626
|
-
const root = taskFolder(tools.
|
|
1648
|
+
const root = taskFolder(tools.dataFolder, job);
|
|
1627
1649
|
const folders = folderNames(job.repositories);
|
|
1628
1650
|
const repositories = [];
|
|
1629
1651
|
for (const [index, repository] of job.repositories.entries()) {
|
|
1630
|
-
const cache = tamsenseFolder(tools.
|
|
1652
|
+
const cache = tamsenseFolder(tools.dataFolder, "repos", repository.fullName.replace("/", "__"));
|
|
1631
1653
|
const folder = folders[index] ?? repository.fullName;
|
|
1632
1654
|
const path = join3(root, folder);
|
|
1633
1655
|
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
@@ -1742,9 +1764,9 @@ async function startRef(source, job, local) {
|
|
|
1742
1764
|
const where = local.remote ? `${source} or on its remote ${local.remote}` : source;
|
|
1743
1765
|
throw new Error(`Branch ${job.branch} is not in ${where}. It was made on another computer: push it to a remote this folder can fetch, or run this check on that computer.`);
|
|
1744
1766
|
}
|
|
1745
|
-
async function releaseBranch2(source, branch, keep,
|
|
1767
|
+
async function releaseBranch2(source, branch, keep, dataFolder) {
|
|
1746
1768
|
const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: source });
|
|
1747
|
-
const work2 = tamsenseFolder(
|
|
1769
|
+
const work2 = tamsenseFolder(dataFolder, "work");
|
|
1748
1770
|
for (const holder of branchHolders(listed.stdout, branch).filter((path) => resolve3(path) !== resolve3(keep))) {
|
|
1749
1771
|
if (!isInside(holder, work2)) {
|
|
1750
1772
|
throw new Error(`Branch ${branch} is checked out in ${resolve3(holder)}. Switch that folder to another branch, then start the step again.`);
|
|
@@ -1789,13 +1811,13 @@ async function prepareRepository(job, repository, path, tools) {
|
|
|
1789
1811
|
if (readsOnly(job)) {
|
|
1790
1812
|
await git(["worktree", "add", "--detach", path, start], { cwd: source });
|
|
1791
1813
|
} else {
|
|
1792
|
-
await releaseBranch2(source, job.branch, path, tools.
|
|
1814
|
+
await releaseBranch2(source, job.branch, path, tools.dataFolder);
|
|
1793
1815
|
await git(["worktree", "add", "-B", job.branch, path, start], { cwd: source });
|
|
1794
1816
|
}
|
|
1795
1817
|
return { baseSha: await git(["rev-parse", "HEAD"], { cwd: path }), local };
|
|
1796
1818
|
}
|
|
1797
1819
|
async function prepareLocalWorkspace(job, tools) {
|
|
1798
|
-
const root = taskFolder(tools.
|
|
1820
|
+
const root = taskFolder(tools.dataFolder, job);
|
|
1799
1821
|
const folders = folderNames(job.repositories);
|
|
1800
1822
|
const repositories = [];
|
|
1801
1823
|
for (const [index, repository] of job.repositories.entries()) {
|
|
@@ -2247,14 +2269,14 @@ function wakeUpLaterTool(tools) {
|
|
|
2247
2269
|
}
|
|
2248
2270
|
);
|
|
2249
2271
|
}
|
|
2250
|
-
async function checkFolder(
|
|
2272
|
+
async function checkFolder(dataFolder, job) {
|
|
2251
2273
|
if (job.taskId !== null) {
|
|
2252
|
-
const step = taskFolder(
|
|
2274
|
+
const step = taskFolder(dataFolder, { taskId: job.taskId, stage: { columnId: job.stepColumnId } });
|
|
2253
2275
|
if (existsSync5(step)) {
|
|
2254
2276
|
return step;
|
|
2255
2277
|
}
|
|
2256
2278
|
}
|
|
2257
|
-
const own = tamsenseFolder(
|
|
2279
|
+
const own = tamsenseFolder(dataFolder, "schedules", `schedule-${job.scheduleId}`);
|
|
2258
2280
|
await mkdir3(own, { recursive: true });
|
|
2259
2281
|
return own;
|
|
2260
2282
|
}
|
|
@@ -3457,12 +3479,16 @@ var runClaude = claudeRunner(query3);
|
|
|
3457
3479
|
|
|
3458
3480
|
// src/config.ts
|
|
3459
3481
|
import { mkdir as mkdir4, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
3460
|
-
import { homedir as homedir2 } from "os";
|
|
3461
3482
|
import { dirname as dirname2, join as join6 } from "path";
|
|
3462
3483
|
var OWNER_ONLY_FILE = 384;
|
|
3463
3484
|
var OWNER_ONLY_FOLDER = 448;
|
|
3464
|
-
|
|
3465
|
-
|
|
3485
|
+
var PROFILES_FOLDER = "profiles";
|
|
3486
|
+
function dataFolderFor(home, profile) {
|
|
3487
|
+
const shared = join6(home, ".tamsense");
|
|
3488
|
+
return profile === null ? shared : join6(shared, PROFILES_FOLDER, profile);
|
|
3489
|
+
}
|
|
3490
|
+
function configPath(dataFolder) {
|
|
3491
|
+
return join6(dataFolder, "runner.json");
|
|
3466
3492
|
}
|
|
3467
3493
|
function isRecord2(value) {
|
|
3468
3494
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -3478,7 +3504,7 @@ function parseConfig(value) {
|
|
|
3478
3504
|
function isMissingFile(error) {
|
|
3479
3505
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
3480
3506
|
}
|
|
3481
|
-
async function readConfig(path
|
|
3507
|
+
async function readConfig(path) {
|
|
3482
3508
|
try {
|
|
3483
3509
|
return parseConfig(JSON.parse(await readFile3(path, "utf8")));
|
|
3484
3510
|
} catch (error) {
|
|
@@ -3488,18 +3514,18 @@ async function readConfig(path = configPath()) {
|
|
|
3488
3514
|
throw error;
|
|
3489
3515
|
}
|
|
3490
3516
|
}
|
|
3491
|
-
async function writeConfig(config, path
|
|
3517
|
+
async function writeConfig(config, path) {
|
|
3492
3518
|
await mkdir4(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
|
|
3493
3519
|
await writeFile3(path, `${JSON.stringify(config, null, 4)}
|
|
3494
3520
|
`, { mode: OWNER_ONLY_FILE });
|
|
3495
3521
|
}
|
|
3496
|
-
async function removeConfig(path
|
|
3522
|
+
async function removeConfig(path) {
|
|
3497
3523
|
await rm4(path, { force: true });
|
|
3498
3524
|
}
|
|
3499
3525
|
|
|
3500
3526
|
// src/login.ts
|
|
3501
3527
|
var MS_PER_SECOND = 1e3;
|
|
3502
|
-
var EXPIRED_TEXT =
|
|
3528
|
+
var EXPIRED_TEXT = "The code expired before it was confirmed. Run the same command again.";
|
|
3503
3529
|
var DENIED_TEXT = "The connection was cancelled in the browser. This computer was not connected.";
|
|
3504
3530
|
async function login(tools, request) {
|
|
3505
3531
|
const pairing = await tools.api.request("POST", PAIRINGS_PATH, request);
|
|
@@ -3864,8 +3890,8 @@ function bundledRecorder() {
|
|
|
3864
3890
|
const path = fileURLToPath(new URL("./recorder.cjs", import.meta.url));
|
|
3865
3891
|
return existsSync6(path) ? path : null;
|
|
3866
3892
|
}
|
|
3867
|
-
function recordingsFolder(
|
|
3868
|
-
return join8(
|
|
3893
|
+
function recordingsFolder(dataFolder, runId) {
|
|
3894
|
+
return join8(dataFolder, "recordings", `run-${runId}`);
|
|
3869
3895
|
}
|
|
3870
3896
|
function nodeOptionsWithRecorder(recorder, nodeOptions) {
|
|
3871
3897
|
return [nodeOptions?.trim(), `--require "${recorder.replaceAll("\\", "/")}"`].filter(Boolean).join(" ");
|
|
@@ -3892,7 +3918,7 @@ function describeDevice(identity) {
|
|
|
3892
3918
|
}
|
|
3893
3919
|
|
|
3894
3920
|
// src/sense.ts
|
|
3895
|
-
import { mkdtemp, rm as rm5 } from "fs/promises";
|
|
3921
|
+
import { mkdtemp, rm as rm5, stat as stat2 } from "fs/promises";
|
|
3896
3922
|
import { tmpdir } from "os";
|
|
3897
3923
|
import { dirname as dirname4, join as join9 } from "path";
|
|
3898
3924
|
import process5 from "process";
|
|
@@ -3912,8 +3938,9 @@ var MAX_NAME_LENGTH2 = 80;
|
|
|
3912
3938
|
function platformLabel(platform = process.platform) {
|
|
3913
3939
|
return PLATFORM_LABELS[platform] ?? platform;
|
|
3914
3940
|
}
|
|
3915
|
-
function defaultDeviceName() {
|
|
3916
|
-
|
|
3941
|
+
function defaultDeviceName(profile = null) {
|
|
3942
|
+
const name = hostname() || "My computer";
|
|
3943
|
+
return (profile === null ? name : `${name} (${profile})`).slice(0, MAX_NAME_LENGTH2);
|
|
3917
3944
|
}
|
|
3918
3945
|
function browserCommand(url, platform) {
|
|
3919
3946
|
if (platform === "win32") {
|
|
@@ -3944,7 +3971,8 @@ async function sleep(ms, signal) {
|
|
|
3944
3971
|
|
|
3945
3972
|
// src/sense.ts
|
|
3946
3973
|
var SERVER_NAME3 = "tamsense";
|
|
3947
|
-
var
|
|
3974
|
+
var WEB_TOOL = "WebFetch";
|
|
3975
|
+
var MAX_SENSE_TURNS = 6;
|
|
3948
3976
|
var HEARTBEAT_MS = 5e3;
|
|
3949
3977
|
var STOPPED_HERE = "stopped_here";
|
|
3950
3978
|
var CLOSED_ON_SITE = "closed_on_site";
|
|
@@ -3962,12 +3990,13 @@ function claudeSenseRunner(startQuery) {
|
|
|
3962
3990
|
options: {
|
|
3963
3991
|
cwd: request.cwd,
|
|
3964
3992
|
systemPrompt: request.system,
|
|
3965
|
-
tools: [],
|
|
3993
|
+
tools: [WEB_TOOL],
|
|
3966
3994
|
settingSources: [],
|
|
3967
3995
|
persistSession: false,
|
|
3996
|
+
...request.resume === null ? {} : { resume: request.resume, forkSession: true },
|
|
3968
3997
|
strictMcpConfig: true,
|
|
3969
3998
|
mcpServers: tools.length > 0 ? { [SERVER_NAME3]: createSdkMcpServer3({ name: SERVER_NAME3, version: "1.0.0", tools }) } : {},
|
|
3970
|
-
allowedTools: tools.map((item) => `mcp__${SERVER_NAME3}__${item.name}`),
|
|
3999
|
+
allowedTools: [WEB_TOOL, ...tools.map((item) => `mcp__${SERVER_NAME3}__${item.name}`)],
|
|
3971
4000
|
permissionMode: "auto",
|
|
3972
4001
|
canUseTool: denyWhenAsked,
|
|
3973
4002
|
maxTurns: MAX_SENSE_TURNS,
|
|
@@ -4000,8 +4029,17 @@ function senseHeartbeat(api, job, abort) {
|
|
|
4000
4029
|
}
|
|
4001
4030
|
};
|
|
4002
4031
|
}
|
|
4032
|
+
async function authorSession(job, dataFolder) {
|
|
4033
|
+
if (!job.author) {
|
|
4034
|
+
return null;
|
|
4035
|
+
}
|
|
4036
|
+
const folder = taskFolder(dataFolder, { taskId: job.taskId, stage: { columnId: job.author.columnId } });
|
|
4037
|
+
const found = await stat2(folder).catch(() => null);
|
|
4038
|
+
return found?.isDirectory() ? { folder, sessionId: job.author.sessionId } : null;
|
|
4039
|
+
}
|
|
4003
4040
|
async function runSenseJob(job, runSense, tools, stop) {
|
|
4004
|
-
const
|
|
4041
|
+
const scratch = await mkdtemp(join9(tmpdir(), "tamsense-sense-"));
|
|
4042
|
+
const author = await authorSession(job, tools.dataFolder);
|
|
4005
4043
|
const run2 = new AbortController();
|
|
4006
4044
|
const stopRun = () => run2.abort(STOPPED_HERE);
|
|
4007
4045
|
if (stop.aborted) {
|
|
@@ -4011,17 +4049,24 @@ async function runSenseJob(job, runSense, tools, stop) {
|
|
|
4011
4049
|
const deadline = setTimeout(() => run2.abort(TOO_LONG), job.deadlineMs);
|
|
4012
4050
|
const beat = senseHeartbeat(tools.api, job, () => run2.abort(CLOSED_ON_SITE));
|
|
4013
4051
|
const beating = setInterval(() => void beat(), HEARTBEAT_MS);
|
|
4014
|
-
let body;
|
|
4052
|
+
let body = FAILED;
|
|
4015
4053
|
try {
|
|
4016
4054
|
const env = withNodeOnPath(process5.env, dirname4(process5.execPath));
|
|
4017
|
-
|
|
4055
|
+
const sources = sourceTools(job.tools, tools.api, sensePath(job.senseId, "sources"));
|
|
4056
|
+
const ask = (cwd, resume) => runSense({ system: job.system, prompt: job.prompt, schema: job.schema, model: job.model, cwd, resume, env, signal: run2.signal, sources }).catch(() => FAILED);
|
|
4057
|
+
if (author) {
|
|
4058
|
+
body = await ask(author.folder, author.sessionId);
|
|
4059
|
+
}
|
|
4060
|
+
if (body.outcome === "failed" && !run2.signal.aborted) {
|
|
4061
|
+
body = await ask(scratch, null);
|
|
4062
|
+
}
|
|
4018
4063
|
} catch {
|
|
4019
4064
|
body = FAILED;
|
|
4020
4065
|
} finally {
|
|
4021
4066
|
clearTimeout(deadline);
|
|
4022
4067
|
clearInterval(beating);
|
|
4023
4068
|
stop.removeEventListener("abort", stopRun);
|
|
4024
|
-
await rm5(
|
|
4069
|
+
await rm5(scratch, { recursive: true, force: true });
|
|
4025
4070
|
}
|
|
4026
4071
|
if (run2.signal.reason === CLOSED_ON_SITE) {
|
|
4027
4072
|
return;
|
|
@@ -4171,8 +4216,8 @@ function allowedSkills(stage) {
|
|
|
4171
4216
|
const skills = stage.role?.skills ?? null;
|
|
4172
4217
|
return skills && [.../* @__PURE__ */ new Set([...skills, ...STAGE_SKILLS[stage.kind] ?? []])].filter((name) => SKILL_NAME.test(name));
|
|
4173
4218
|
}
|
|
4174
|
-
function roleLibraryFolder(
|
|
4175
|
-
return join10(
|
|
4219
|
+
function roleLibraryFolder(dataFolder, runId) {
|
|
4220
|
+
return join10(dataFolder, "libraries", `run-${runId}`);
|
|
4176
4221
|
}
|
|
4177
4222
|
async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
|
|
4178
4223
|
if (skills === null) {
|
|
@@ -4191,7 +4236,7 @@ async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
|
|
|
4191
4236
|
// src/recordings.ts
|
|
4192
4237
|
import { randomUUID } from "crypto";
|
|
4193
4238
|
import { createReadStream } from "fs";
|
|
4194
|
-
import { readdir as readdir2, readFile as readFile4, stat as
|
|
4239
|
+
import { readdir as readdir2, readFile as readFile4, stat as stat3, writeFile as writeFile4 } from "fs/promises";
|
|
4195
4240
|
import { basename as basename4, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute3, join as join11 } from "path";
|
|
4196
4241
|
import { createInterface as createInterface2 } from "readline";
|
|
4197
4242
|
import { Readable } from "stream";
|
|
@@ -4298,7 +4343,7 @@ async function remember(dir, file) {
|
|
|
4298
4343
|
await writeFile4(join11(dir, LEDGER_FILE), JSON.stringify([...done2]));
|
|
4299
4344
|
}
|
|
4300
4345
|
async function nonEmpty(file) {
|
|
4301
|
-
return file !== null && ((await
|
|
4346
|
+
return file !== null && ((await stat3(file).catch(() => null))?.size ?? 0) > 0;
|
|
4302
4347
|
}
|
|
4303
4348
|
async function pendingRecordings(dir) {
|
|
4304
4349
|
const done2 = await ledger(dir);
|
|
@@ -4333,7 +4378,7 @@ async function uploadReplay(upload, file, label) {
|
|
|
4333
4378
|
}
|
|
4334
4379
|
async function uploadVideo(upload, file, label, meta) {
|
|
4335
4380
|
const contentType = VIDEO_TYPES_BY_EXTENSION[extname2(file).toLowerCase()];
|
|
4336
|
-
const info = await
|
|
4381
|
+
const info = await stat3(file);
|
|
4337
4382
|
if (info.size > MAX_VIDEO_BYTES) {
|
|
4338
4383
|
throw new Error(`The video is larger than ${MAX_VIDEO_BYTES / 1024 / 1024} MB. Record a shorter one.`);
|
|
4339
4384
|
}
|
|
@@ -4422,7 +4467,7 @@ async function uploadPendingRecordings(api, runId, dir) {
|
|
|
4422
4467
|
}
|
|
4423
4468
|
|
|
4424
4469
|
// src/screenshot.ts
|
|
4425
|
-
import { readFile as readFile5, stat as
|
|
4470
|
+
import { readFile as readFile5, stat as stat4 } from "fs/promises";
|
|
4426
4471
|
import { basename as basename5, extname as extname3, isAbsolute as isAbsolute4 } from "path";
|
|
4427
4472
|
var TYPES_BY_EXTENSION2 = {
|
|
4428
4473
|
".png": "image/png",
|
|
@@ -4444,7 +4489,7 @@ async function readScreenshot(request) {
|
|
|
4444
4489
|
} catch (error) {
|
|
4445
4490
|
return error instanceof Error ? error.message : String(error);
|
|
4446
4491
|
}
|
|
4447
|
-
const info = await
|
|
4492
|
+
const info = await stat4(request.path).catch(() => null);
|
|
4448
4493
|
if (!info?.isFile()) {
|
|
4449
4494
|
return `There is no file at ${request.path}.`;
|
|
4450
4495
|
}
|
|
@@ -4655,7 +4700,7 @@ function skillsLine(skills) {
|
|
|
4655
4700
|
async function prepare(job, tools) {
|
|
4656
4701
|
if (isFolderJob(job)) {
|
|
4657
4702
|
tools.print("Preparing the task folder\u2026");
|
|
4658
|
-
const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.
|
|
4703
|
+
const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.dataFolder, job), tools.api);
|
|
4659
4704
|
return { workspace: workspace2, results, identity: {} };
|
|
4660
4705
|
}
|
|
4661
4706
|
const taskResults = (workspace2) => writeResults(join12(workspace2.root, RESULTS_FOLDER), job, tools.api);
|
|
@@ -4678,12 +4723,12 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
4678
4723
|
if (skills) {
|
|
4679
4724
|
tools.print(skillsLine(skills));
|
|
4680
4725
|
}
|
|
4681
|
-
const library = await prepareLibrary(roleLibraryFolder(tools.
|
|
4726
|
+
const library = await prepareLibrary(roleLibraryFolder(tools.dataFolder, job.runId), skills);
|
|
4682
4727
|
signal.throwIfAborted();
|
|
4683
4728
|
tools.print("Claude is working\u2026");
|
|
4684
4729
|
const attached = { count: 0 };
|
|
4685
4730
|
const session = sessionReporter(tools.api, job, tools.print);
|
|
4686
|
-
const recordings = recordingsFolder(tools.
|
|
4731
|
+
const recordings = recordingsFolder(tools.dataFolder, job.runId);
|
|
4687
4732
|
const baseEnv = withNodeOnPath(process7.env, dirname6(process7.execPath));
|
|
4688
4733
|
const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
|
|
4689
4734
|
const outcome = await tools.runAgent({
|
|
@@ -4770,7 +4815,7 @@ function resultLine(finish2, workspace = "repositories") {
|
|
|
4770
4815
|
return `Done. Pushed ${finish2.pushed.length} branch(es), the pull requests open on TAMsense.`;
|
|
4771
4816
|
}
|
|
4772
4817
|
async function sendRecordings(job, tools) {
|
|
4773
|
-
const folder = recordingsFolder(tools.
|
|
4818
|
+
const folder = recordingsFolder(tools.dataFolder, job.runId);
|
|
4774
4819
|
try {
|
|
4775
4820
|
const report = await uploadPendingRecordings(tools.api, job.runId, folder);
|
|
4776
4821
|
if (report.uploaded > 0) {
|
|
@@ -4811,7 +4856,7 @@ async function runJob(job, tools, stop) {
|
|
|
4811
4856
|
} catch (error) {
|
|
4812
4857
|
finish2 = { outcome: "failed", report: failureReport(error, run2.signal), pushed: [], model: null, usage: null, verdict: null, handoff: null, sessionId: null, pausedUntil: null, questions: null, spec: null, waitingFor: null };
|
|
4813
4858
|
} finally {
|
|
4814
|
-
await rm7(roleLibraryFolder(tools.
|
|
4859
|
+
await rm7(roleLibraryFolder(tools.dataFolder, job.runId), { recursive: true, force: true });
|
|
4815
4860
|
clearInterval(polling);
|
|
4816
4861
|
clearInterval(feeding);
|
|
4817
4862
|
stop.removeEventListener("abort", stopRun);
|
|
@@ -4823,7 +4868,7 @@ async function runJob(job, tools, stop) {
|
|
|
4823
4868
|
}
|
|
4824
4869
|
await feed.flush();
|
|
4825
4870
|
if (run2.signal.reason === CLOSED_ON_SITE2) {
|
|
4826
|
-
await rm7(recordingsFolder(tools.
|
|
4871
|
+
await rm7(recordingsFolder(tools.dataFolder, job.runId), { recursive: true, force: true });
|
|
4827
4872
|
tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
|
|
4828
4873
|
return;
|
|
4829
4874
|
}
|
|
@@ -4854,7 +4899,7 @@ async function runCheckJob(check, runCheck, tools, stop) {
|
|
|
4854
4899
|
const beating = setInterval(() => void beat(), CHECK_HEARTBEAT_MS);
|
|
4855
4900
|
let body;
|
|
4856
4901
|
try {
|
|
4857
|
-
const folder = await checkFolder(tools.
|
|
4902
|
+
const folder = await checkFolder(tools.dataFolder, check);
|
|
4858
4903
|
const env = withNodeOnPath(process7.env, dirname6(process7.execPath));
|
|
4859
4904
|
const prompt = routine ? buildRoutinePrompt(check, folder) : buildCheckPrompt(check, folder);
|
|
4860
4905
|
body = checkFinishBody(await runCheck({ cwd: folder, prompt, model: check.model, env, signal: run2.signal, sources: sourceTools(check.sourceTools, tools.api, checkPath(check.checkId, "sources")), routine }));
|
|
@@ -4994,11 +5039,18 @@ async function work(tools, stop, parallel = 1) {
|
|
|
4994
5039
|
}
|
|
4995
5040
|
|
|
4996
5041
|
// src/cli.ts
|
|
4997
|
-
var NOT_CONNECTED_TEXT = `This computer is not connected. Run ${LOGIN_COMMAND}`;
|
|
4998
|
-
var DISCONNECTED_TEXT = `This computer was disconnected on the website. Run ${LOGIN_COMMAND}`;
|
|
4999
5042
|
var NO_TASKS_ACCESS_TEXT = "Your account no longer has access to Tasks. Ask the project owner.";
|
|
5000
|
-
var
|
|
5043
|
+
var UNAUTHORIZED_STATUS = 401;
|
|
5044
|
+
var FORBIDDEN_STATUS = 403;
|
|
5001
5045
|
var INTERRUPTED_EXIT_CODE = 130;
|
|
5046
|
+
function connectionFor(profile) {
|
|
5047
|
+
const dataFolder = dataFolderFor(homedir2(), profile);
|
|
5048
|
+
return { profile, dataFolder, configFile: configPath(dataFolder) };
|
|
5049
|
+
}
|
|
5050
|
+
function notConnectedText(profile) {
|
|
5051
|
+
const which = profile === null ? "" : ` in the profile ${profile}`;
|
|
5052
|
+
return `This computer is not connected${which}. Run ${withProfile(LOGIN_COMMAND, profile)}`;
|
|
5053
|
+
}
|
|
5002
5054
|
function print(line) {
|
|
5003
5055
|
process8.stdout.write(`${line}
|
|
5004
5056
|
`);
|
|
@@ -5006,27 +5058,33 @@ function print(line) {
|
|
|
5006
5058
|
function serverFor(flag, saved) {
|
|
5007
5059
|
return serverOrigin(flag ?? process8.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
|
|
5008
5060
|
}
|
|
5009
|
-
function accessProblem(error) {
|
|
5010
|
-
|
|
5061
|
+
function accessProblem(error, profile) {
|
|
5062
|
+
if (!(error instanceof ApiError)) {
|
|
5063
|
+
return null;
|
|
5064
|
+
}
|
|
5065
|
+
if (error.status === UNAUTHORIZED_STATUS) {
|
|
5066
|
+
return `This computer was disconnected on the website. Run ${withProfile(LOGIN_COMMAND, profile)}`;
|
|
5067
|
+
}
|
|
5068
|
+
return error.status === FORBIDDEN_STATUS ? NO_TASKS_ACCESS_TEXT : null;
|
|
5011
5069
|
}
|
|
5012
|
-
async function forgetDevice(config) {
|
|
5070
|
+
async function forgetDevice(config, connection) {
|
|
5013
5071
|
await createApi(config.server, config.token).request("DELETE", IDENTITY_PATH).catch(() => {
|
|
5014
5072
|
});
|
|
5015
|
-
await removeConfig();
|
|
5073
|
+
await removeConfig(connection.configFile);
|
|
5016
5074
|
}
|
|
5017
|
-
async function runLogin(server, deviceName) {
|
|
5018
|
-
const saved = await readConfig();
|
|
5075
|
+
async function runLogin(connection, server, deviceName) {
|
|
5076
|
+
const saved = await readConfig(connection.configFile);
|
|
5019
5077
|
const api = createApi(serverFor(server, saved));
|
|
5020
5078
|
if (saved) {
|
|
5021
|
-
await forgetDevice(saved);
|
|
5079
|
+
await forgetDevice(saved, connection);
|
|
5022
5080
|
}
|
|
5023
|
-
const name = deviceName?.trim() || defaultDeviceName();
|
|
5081
|
+
const name = deviceName?.trim() || defaultDeviceName(connection.profile);
|
|
5024
5082
|
const result = await login(
|
|
5025
5083
|
{ api, openBrowser, print, sleep, now: Date.now },
|
|
5026
5084
|
{ name, platform: platformLabel() }
|
|
5027
5085
|
);
|
|
5028
|
-
await writeConfig({ server: api.server, token: result.token, deviceId: result.device.id, deviceName: result.device.name });
|
|
5029
|
-
print(`Connected: ${result.device.name}. To take your tasks, go to your code folder and run ${START_COMMAND}`);
|
|
5086
|
+
await writeConfig({ server: api.server, token: result.token, deviceId: result.device.id, deviceName: result.device.name }, connection.configFile);
|
|
5087
|
+
print(`Connected: ${result.device.name}. To take your tasks, go to your code folder and run ${withProfile(START_COMMAND, connection.profile)}`);
|
|
5030
5088
|
}
|
|
5031
5089
|
function stopOnInterrupt() {
|
|
5032
5090
|
const stop = new AbortController();
|
|
@@ -5061,13 +5119,13 @@ async function startedNewVersion() {
|
|
|
5061
5119
|
return false;
|
|
5062
5120
|
}
|
|
5063
5121
|
}
|
|
5064
|
-
async function runStart(parallel, installsBrowser) {
|
|
5122
|
+
async function runStart(connection, parallel, installsBrowser) {
|
|
5065
5123
|
if (await startedNewVersion()) {
|
|
5066
5124
|
return;
|
|
5067
5125
|
}
|
|
5068
|
-
const saved = await readConfig();
|
|
5126
|
+
const saved = await readConfig(connection.configFile);
|
|
5069
5127
|
if (!saved) {
|
|
5070
|
-
print(
|
|
5128
|
+
print(notConnectedText(connection.profile));
|
|
5071
5129
|
return;
|
|
5072
5130
|
}
|
|
5073
5131
|
const identity = await createApi(saved.server, saved.token).request("GET", IDENTITY_PATH);
|
|
@@ -5076,13 +5134,13 @@ async function runStart(parallel, installsBrowser) {
|
|
|
5076
5134
|
throw new Error(problem2);
|
|
5077
5135
|
}
|
|
5078
5136
|
print(`${identity.device.name} is connected to ${saved.server} as ${describeDevice(identity)}.`);
|
|
5079
|
-
const home =
|
|
5137
|
+
const home = homedir2();
|
|
5080
5138
|
const browser = await browserForStart(home, installsBrowser);
|
|
5081
5139
|
const api = createApi(saved.server, saved.token, { watch: connectionWatch(print) });
|
|
5082
|
-
await work({ api, runAgent: runClaude, runCheck: runClaudeCheck, runSense: runClaudeSense, version: currentVersion() ?? void 0, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home), startFolder: process8.cwd() }, stopOnInterrupt(), parallel);
|
|
5140
|
+
await work({ api, runAgent: runClaude, runCheck: runClaudeCheck, runSense: runClaudeSense, version: currentVersion() ?? void 0, capabilities: probeCapabilities, print, sleep, home, dataFolder: connection.dataFolder, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home), startFolder: process8.cwd() }, stopOnInterrupt(), parallel);
|
|
5083
5141
|
}
|
|
5084
5142
|
async function runBrowser(remove) {
|
|
5085
|
-
const home =
|
|
5143
|
+
const home = homedir2();
|
|
5086
5144
|
if (remove) {
|
|
5087
5145
|
print(await removeBrowser(home) ? "Removed the headless browser. The next start installs it again, unless you add --no-browser." : "No headless browser is installed.");
|
|
5088
5146
|
return;
|
|
@@ -5095,46 +5153,53 @@ async function runBrowser(remove) {
|
|
|
5095
5153
|
await installBrowser(installTools(home));
|
|
5096
5154
|
print("Claude uses it for interface checks from the next start.");
|
|
5097
5155
|
}
|
|
5098
|
-
async function runStatus() {
|
|
5099
|
-
const saved = await readConfig();
|
|
5156
|
+
async function runStatus(connection) {
|
|
5157
|
+
const saved = await readConfig(connection.configFile);
|
|
5100
5158
|
if (!saved) {
|
|
5101
|
-
print(
|
|
5159
|
+
print(notConnectedText(connection.profile));
|
|
5102
5160
|
return;
|
|
5103
5161
|
}
|
|
5104
5162
|
const identity = await createApi(saved.server, saved.token).request("GET", IDENTITY_PATH);
|
|
5105
5163
|
print(`Connected: ${identity.device.name}, ${describeDevice(identity)}`);
|
|
5106
5164
|
print(`Account: ${identity.email ?? "unknown"}, project ${identity.projectName}`);
|
|
5107
5165
|
print(`Server: ${saved.server}`);
|
|
5166
|
+
if (connection.profile !== null) {
|
|
5167
|
+
print(`Profile: ${connection.profile}, folders in ${connection.dataFolder}`);
|
|
5168
|
+
}
|
|
5108
5169
|
}
|
|
5109
|
-
async function runLogout() {
|
|
5110
|
-
const saved = await readConfig();
|
|
5170
|
+
async function runLogout(connection) {
|
|
5171
|
+
const saved = await readConfig(connection.configFile);
|
|
5111
5172
|
if (!saved) {
|
|
5112
|
-
print(
|
|
5173
|
+
print(notConnectedText(connection.profile));
|
|
5113
5174
|
return;
|
|
5114
5175
|
}
|
|
5115
|
-
await forgetDevice(saved);
|
|
5176
|
+
await forgetDevice(saved, connection);
|
|
5116
5177
|
print("Disconnected. This computer no longer takes your tasks.");
|
|
5117
5178
|
}
|
|
5118
5179
|
async function run(command) {
|
|
5119
5180
|
if (command.name === "login") {
|
|
5120
|
-
await runLogin(command.server, command.deviceName);
|
|
5181
|
+
await runLogin(connectionFor(command.profile), command.server, command.deviceName);
|
|
5121
5182
|
} else if (command.name === "start") {
|
|
5122
|
-
await runStart(command.parallel, command.browser);
|
|
5183
|
+
await runStart(connectionFor(command.profile), command.parallel, command.browser);
|
|
5123
5184
|
} else if (command.name === "browser") {
|
|
5124
5185
|
await runBrowser(command.remove);
|
|
5125
5186
|
} else if (command.name === "status") {
|
|
5126
|
-
await runStatus();
|
|
5187
|
+
await runStatus(connectionFor(command.profile));
|
|
5127
5188
|
} else if (command.name === "logout") {
|
|
5128
|
-
await runLogout();
|
|
5189
|
+
await runLogout(connectionFor(command.profile));
|
|
5129
5190
|
} else {
|
|
5130
5191
|
print(HELP_TEXT);
|
|
5131
5192
|
}
|
|
5132
5193
|
}
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
} catch (error) {
|
|
5136
|
-
process8.stderr.write(`${accessProblem(error) ?? (error instanceof Error ? error.message : String(error))}
|
|
5194
|
+
function fail(error, profile) {
|
|
5195
|
+
process8.stderr.write(`${accessProblem(error, profile) ?? (error instanceof Error ? error.message : String(error))}
|
|
5137
5196
|
`);
|
|
5138
5197
|
process8.exitCode = 1;
|
|
5139
5198
|
}
|
|
5199
|
+
try {
|
|
5200
|
+
const command = parseCommand(process8.argv.slice(2), process8.env);
|
|
5201
|
+
await run(command).catch((error) => fail(error, "profile" in command ? command.profile : null));
|
|
5202
|
+
} catch (error) {
|
|
5203
|
+
fail(error, null);
|
|
5204
|
+
}
|
|
5140
5205
|
//# sourceMappingURL=cli.js.map
|