@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 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 homedir3 } from "os";
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(home, ...parts) {
1558
- return join3(home, ".tamsense", ...parts);
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(home, job) {
1623
- return tamsenseFolder(home, "work", ...stepFolder(job));
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.home, job);
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.home, "repos", repository.fullName.replace("/", "__"));
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, home) {
1767
+ async function releaseBranch2(source, branch, keep, dataFolder) {
1746
1768
  const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: source });
1747
- const work2 = tamsenseFolder(home, "work");
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.home);
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.home, job);
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(home, job) {
2272
+ async function checkFolder(dataFolder, job) {
2251
2273
  if (job.taskId !== null) {
2252
- const step = taskFolder(home, { taskId: job.taskId, stage: { columnId: job.stepColumnId } });
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(home, "schedules", `schedule-${job.scheduleId}`);
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
- function configPath(home = homedir2()) {
3465
- return join6(home, ".tamsense", "runner.json");
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 = configPath()) {
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 = configPath()) {
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 = configPath()) {
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 = `The code expired before it was confirmed. Run ${LOGIN_COMMAND} again.`;
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(home, runId) {
3868
- return join8(home, ".tamsense", "recordings", `run-${runId}`);
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
- return hostname().slice(0, MAX_NAME_LENGTH2) || "My computer";
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 MAX_SENSE_TURNS = 3;
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 cwd = await mkdtemp(join9(tmpdir(), "tamsense-sense-"));
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
- body = await runSense({ system: job.system, prompt: job.prompt, schema: job.schema, model: job.model, cwd, env, signal: run2.signal, sources: sourceTools(job.tools, tools.api, sensePath(job.senseId, "sources")) });
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(cwd, { recursive: true, force: true });
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(home, runId) {
4175
- return join10(home, ".tamsense", "libraries", `run-${runId}`);
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 stat2, writeFile as writeFile4 } from "fs/promises";
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 stat2(file).catch(() => null))?.size ?? 0) > 0;
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 stat2(file);
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 stat3 } from "fs/promises";
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 stat3(request.path).catch(() => null);
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.home, job), tools.api);
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.home, job.runId), skills);
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.home, job.runId);
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.home, job.runId);
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.home, job.runId), { recursive: true, force: true });
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.home, job.runId), { recursive: true, force: true });
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.home, check);
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 ACCESS_PROBLEMS = { 401: DISCONNECTED_TEXT, 403: NO_TASKS_ACCESS_TEXT };
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
- return error instanceof ApiError ? ACCESS_PROBLEMS[error.status] ?? null : null;
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(NOT_CONNECTED_TEXT);
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 = homedir3();
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 = homedir3();
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(NOT_CONNECTED_TEXT);
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(NOT_CONNECTED_TEXT);
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
- try {
5134
- await run(parseCommand(process8.argv.slice(2), process8.env));
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