@task0/cli 0.11.0 → 0.12.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.
Files changed (2) hide show
  1. package/dist/main.js +166 -15
  2. package/package.json +1 -1
package/dist/main.js CHANGED
@@ -4763,6 +4763,9 @@ function emitAgentRunStatus(runId, status, opts = {}) {
4763
4763
  phase: opts.phase ?? null,
4764
4764
  exit_code: opts.exitCode ?? null,
4765
4765
  error: opts.error ?? null,
4766
+ agent_session_id: opts.agentSessionId ?? null,
4767
+ agent_project: opts.agentProject ?? null,
4768
+ metadata: opts.metadata ?? null,
4766
4769
  ts: (/* @__PURE__ */ new Date()).toISOString()
4767
4770
  });
4768
4771
  if (status === "completed" || status === "failed" || status === "killed") {
@@ -4872,6 +4875,28 @@ function checkTmuxAvailable() {
4872
4875
  return { ok: false, reason: `tmux not available: ${err instanceof Error ? err.message : String(err)}` };
4873
4876
  }
4874
4877
  }
4878
+ function injectNodeBin(scriptContent) {
4879
+ const nodeBin = process.execPath;
4880
+ const nodeDir = path25.dirname(nodeBin);
4881
+ const home = process.env.HOME ?? "";
4882
+ const extraPath = [
4883
+ nodeDir,
4884
+ home ? `${home}/.local/share/mise/shims` : "",
4885
+ home ? `${home}/.local/bin` : "",
4886
+ home ? `${home}/bin` : "",
4887
+ home ? `${home}/.cargo/bin` : ""
4888
+ ].filter(Boolean).join(":");
4889
+ const injected = [
4890
+ `NODE_BIN=${JSON.stringify(nodeBin)}`,
4891
+ `export PATH=${JSON.stringify(extraPath)}:"$PATH"`,
4892
+ ""
4893
+ ].join("\n");
4894
+ const shebangMatch = scriptContent.match(/^(#![^\n]*\n)/);
4895
+ if (shebangMatch) {
4896
+ return shebangMatch[1] + injected + scriptContent.slice(shebangMatch[1].length);
4897
+ }
4898
+ return injected + scriptContent;
4899
+ }
4875
4900
  function isSessionAlive2(sessionName) {
4876
4901
  try {
4877
4902
  execFileSync(getTmuxBin(), ["has-session", "-t", sessionName], {
@@ -4911,7 +4936,17 @@ function deriveStatus(parsed) {
4911
4936
  else if (raw === "error" || raw === "failed") status = "failed";
4912
4937
  const phase = typeof parsed.phase === "string" ? parsed.phase : null;
4913
4938
  const error2 = typeof parsed.error === "string" && parsed.error ? parsed.error : null;
4914
- return { status, phase, error: error2 };
4939
+ const agentSessionId = typeof parsed.agentSessionId === "string" && parsed.agentSessionId ? parsed.agentSessionId : null;
4940
+ const agentProject = typeof parsed.agentProject === "string" && parsed.agentProject ? parsed.agentProject : null;
4941
+ let metadata = null;
4942
+ if (parsed.metadata && typeof parsed.metadata === "object") {
4943
+ const out = {};
4944
+ for (const [k, v] of Object.entries(parsed.metadata)) {
4945
+ if (typeof k === "string" && typeof v === "string") out[k] = v;
4946
+ }
4947
+ if (Object.keys(out).length > 0) metadata = out;
4948
+ }
4949
+ return { status, phase, error: error2, agentSessionId, agentProject, metadata };
4915
4950
  }
4916
4951
  function launchAgentRun(params) {
4917
4952
  const tmuxCheck = checkTmuxAvailable();
@@ -4926,9 +4961,11 @@ function launchAgentRun(params) {
4926
4961
  }
4927
4962
  const runDir = ensureAgentRunDir(params.runId);
4928
4963
  const scriptPath = path25.join(runDir, "script.sh");
4929
- fs27.writeFileSync(scriptPath, params.scriptContent, { mode: 493 });
4964
+ const scriptWithNode = injectNodeBin(params.scriptContent);
4965
+ fs27.writeFileSync(scriptPath, scriptWithNode, { mode: 493 });
4930
4966
  if (params.promptContent !== void 0) {
4931
- fs27.writeFileSync(path25.join(runDir, "prompt.txt"), params.promptContent, "utf-8");
4967
+ const expanded = params.promptContent.replace(/__TASK0_AGENT_RUN_DIR__/g, runDir);
4968
+ fs27.writeFileSync(path25.join(runDir, "prompt.txt"), expanded, "utf-8");
4932
4969
  }
4933
4970
  for (const [name, content] of Object.entries(params.auxFiles ?? {})) {
4934
4971
  if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
@@ -4970,7 +5007,20 @@ function launchAgentRun(params) {
4970
5007
  return;
4971
5008
  }
4972
5009
  if (!isSessionAlive2(cur.sessionName)) {
4973
- finishRun(params.runId, "killed", { error: "tmux session disappeared" });
5010
+ const final = readStatusFile(params.runId);
5011
+ const derived = final ? deriveStatus(final.parsed) : null;
5012
+ if (derived && (derived.status === "completed" || derived.status === "failed")) {
5013
+ emitAgentRunStatus(params.runId, derived.status, {
5014
+ phase: derived.phase,
5015
+ error: derived.error,
5016
+ agentSessionId: derived.agentSessionId,
5017
+ agentProject: derived.agentProject,
5018
+ metadata: derived.metadata
5019
+ });
5020
+ finishRun(params.runId, derived.status, { phase: derived.phase, error: derived.error });
5021
+ } else {
5022
+ finishRun(params.runId, "killed", { error: "tmux session disappeared" });
5023
+ }
4974
5024
  clearInterval(sessionProbe);
4975
5025
  }
4976
5026
  }, SESSION_PROBE_MS);
@@ -4983,10 +5033,16 @@ function pollOne(runId) {
4983
5033
  if (!current) return;
4984
5034
  if (current.raw === entry.lastStatusJson) return;
4985
5035
  entry.lastStatusJson = current.raw;
4986
- const { status, phase, error: error2 } = deriveStatus(current.parsed);
4987
- emitAgentRunStatus(runId, status, { phase, error: error2 });
4988
- if (status === "completed" || status === "failed") {
4989
- finishRun(runId, status, { phase, error: error2 });
5036
+ const derived = deriveStatus(current.parsed);
5037
+ emitAgentRunStatus(runId, derived.status, {
5038
+ phase: derived.phase,
5039
+ error: derived.error,
5040
+ agentSessionId: derived.agentSessionId,
5041
+ agentProject: derived.agentProject,
5042
+ metadata: derived.metadata
5043
+ });
5044
+ if (derived.status === "completed" || derived.status === "failed") {
5045
+ finishRun(runId, derived.status, { phase: derived.phase, error: derived.error });
4990
5046
  }
4991
5047
  }
4992
5048
  function finishRun(runId, status, opts = {}) {
@@ -6093,6 +6149,55 @@ function run2(cmd, args) {
6093
6149
  const res = spawnSync6(cmd, args, { encoding: "utf-8" });
6094
6150
  return { code: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
6095
6151
  }
6152
+ function sleep2(ms) {
6153
+ return new Promise((resolve) => setTimeout(resolve, ms));
6154
+ }
6155
+ function launchdServicePid(scope) {
6156
+ const res = run2("launchctl", ["print", serviceTarget(scope)]);
6157
+ if (res.code !== 0) return null;
6158
+ const m = res.stdout.match(/\bpid\s*=\s*(\d+)/);
6159
+ if (!m) return null;
6160
+ const pid = parseInt(m[1], 10);
6161
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
6162
+ }
6163
+ function pidAlive(pid) {
6164
+ try {
6165
+ process.kill(pid, 0);
6166
+ return true;
6167
+ } catch (err) {
6168
+ return err.code === "EPERM";
6169
+ }
6170
+ }
6171
+ function findOrphanDaemonPids(mainPath, excludePid) {
6172
+ const res = run2("ps", ["-Ewwo", "pid=,command="]);
6173
+ if (res.code !== 0) return [];
6174
+ const expectedHome = process.env.TASK0_HOME ?? "";
6175
+ const pids = [];
6176
+ for (const line of res.stdout.split("\n")) {
6177
+ const m = line.match(/^\s*(\d+)\s+(.*)$/);
6178
+ if (!m) continue;
6179
+ const pid = parseInt(m[1], 10);
6180
+ if (!Number.isFinite(pid) || pid <= 0) continue;
6181
+ if (excludePid !== null && pid === excludePid) continue;
6182
+ if (pid === process.pid) continue;
6183
+ const cmd = m[2];
6184
+ if (!cmd.includes(mainPath)) continue;
6185
+ if (!/\bdaemon\s+run\b/.test(cmd)) continue;
6186
+ const homeMatch = cmd.match(/\bTASK0_HOME=([^\s]*)/);
6187
+ const actualHome = homeMatch ? homeMatch[1] : "";
6188
+ if (actualHome !== expectedHome) continue;
6189
+ pids.push(pid);
6190
+ }
6191
+ return pids;
6192
+ }
6193
+ async function waitForPidExit(pid, timeoutMs) {
6194
+ const start = Date.now();
6195
+ while (Date.now() - start < timeoutMs) {
6196
+ if (!pidAlive(pid)) return true;
6197
+ await sleep2(100);
6198
+ }
6199
+ return false;
6200
+ }
6096
6201
  function createLaunchdManager(scope) {
6097
6202
  const file = unitPath(scope);
6098
6203
  const logs = logPaths();
@@ -6141,19 +6246,65 @@ function createLaunchdManager(scope) {
6141
6246
  }
6142
6247
  }
6143
6248
  async function start() {
6144
- const res = run2("launchctl", ["kickstart", "-k", serviceTarget(scope)]);
6145
- if (res.code !== 0) {
6146
- const load = run2("launchctl", ["load", "-w", file]);
6147
- if (load.code !== 0) {
6148
- throw new Error(`launchctl start failed: ${res.stderr || load.stderr}`);
6249
+ if (!fs30.existsSync(file)) {
6250
+ throw new Error(
6251
+ `service unit ${file} is missing \u2014 run \`task0 daemon register\` first to install it`
6252
+ );
6253
+ }
6254
+ const kick = run2("launchctl", ["kickstart", "-k", serviceTarget(scope)]);
6255
+ if (kick.code !== 0) {
6256
+ const bootstrap = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
6257
+ if (bootstrap.code !== 0) {
6258
+ const legacy = run2("launchctl", ["load", "-w", file]);
6259
+ if (legacy.code !== 0) {
6260
+ throw new Error(
6261
+ `launchctl start failed: ${kick.stderr || bootstrap.stderr || legacy.stderr}`
6262
+ );
6263
+ }
6149
6264
  }
6150
6265
  }
6266
+ for (let i = 0; i < 30; i += 1) {
6267
+ if (launchdServicePid(scope) != null) return;
6268
+ await sleep2(100);
6269
+ }
6270
+ throw new Error(
6271
+ `daemon did not start within 3s \u2014 check logs at ${logs.err}`
6272
+ );
6151
6273
  }
6152
6274
  async function stop() {
6153
- const res = run2("launchctl", ["kill", "SIGTERM", serviceTarget(scope)]);
6154
- if (res.code !== 0) {
6275
+ const launchdPid = launchdServicePid(scope);
6276
+ const bootout = run2("launchctl", ["bootout", serviceTarget(scope)]);
6277
+ if (bootout.code !== 0) {
6155
6278
  run2("launchctl", ["unload", file]);
6156
6279
  }
6280
+ if (launchdPid !== null) {
6281
+ const exited = await waitForPidExit(launchdPid, 5e3);
6282
+ if (!exited) {
6283
+ try {
6284
+ process.kill(launchdPid, "SIGKILL");
6285
+ } catch {
6286
+ }
6287
+ }
6288
+ }
6289
+ const inv = resolveTask0Invocation();
6290
+ const orphans = findOrphanDaemonPids(inv.main, launchdPid);
6291
+ for (const pid of orphans) {
6292
+ try {
6293
+ process.kill(pid, "SIGTERM");
6294
+ } catch {
6295
+ }
6296
+ }
6297
+ if (orphans.length > 0) {
6298
+ await sleep2(500);
6299
+ for (const pid of orphans) {
6300
+ if (pidAlive(pid)) {
6301
+ try {
6302
+ process.kill(pid, "SIGKILL");
6303
+ } catch {
6304
+ }
6305
+ }
6306
+ }
6307
+ }
6157
6308
  }
6158
6309
  async function status() {
6159
6310
  if (!fs30.existsSync(file)) return "absent";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@task0/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "task0 — task-centric agent workflow CLI",
5
5
  "homepage": "https://github.com/cy0-labs/task0#readme",
6
6
  "repository": {