@todos-dev/cli 0.1.1 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +215 -284
  2. package/package.json +10 -10
package/dist/index.js CHANGED
@@ -119,19 +119,23 @@ async function withRetry(fn, shouldRetry, label, delays = DEFAULT_RETRY_DELAYS_M
119
119
  function workspacesRoot() {
120
120
  return process.env.TDS_WORKSPACES_DIR ?? (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".tds", "workspaces");
121
121
  }
122
- function spawnGit(args2, cwd) {
122
+ function gitEnv(config2) {
123
+ const entries = Object.entries({ "credential.helper": "", ...config2 });
124
+ const env = { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_CONFIG_COUNT: String(entries.length) };
125
+ entries.forEach(([key, value], i) => {
126
+ env[`GIT_CONFIG_KEY_${i}`] = key;
127
+ env[`GIT_CONFIG_VALUE_${i}`] = value;
128
+ });
129
+ return env;
130
+ }
131
+ function authConfig(token) {
132
+ return { [`url.https://x-access-token:${token}@github.com/.insteadOf`]: "https://github.com/" };
133
+ }
134
+ function spawnGit(args2, cwd, config2 = {}) {
123
135
  return new Promise((resolve, reject) => {
124
136
  const child = (0, import_node_child_process.spawn)("git", args2, {
125
137
  cwd,
126
- env: {
127
- ...process.env,
128
- GIT_TERMINAL_PROMPT: "0",
129
- // Override any system credential helper (e.g. gh auth git-credential) so
130
- // git uses the token embedded in the URL directly and never calls an external helper.
131
- GIT_CONFIG_COUNT: "1",
132
- GIT_CONFIG_KEY_0: "credential.helper",
133
- GIT_CONFIG_VALUE_0: ""
134
- },
138
+ env: gitEnv(config2),
135
139
  stdio: ["ignore", "pipe", "pipe"]
136
140
  });
137
141
  let stdout = "";
@@ -154,9 +158,9 @@ function isTransientGitError(err) {
154
158
  const msg = err instanceof Error ? err.message : String(err);
155
159
  return /SSL_ERROR_SYSCALL|SSL_connect|gnutls_handshake|[Cc]ould not resolve host|Temporary failure in name resolution|Connection (timed out|reset|refused)|Operation timed out|Failed to connect|[Rr]ecv failure|[Ss]end failure|early EOF|RPC failed|unexpected disconnect|remote end hung up|The requested URL returned error: 5\d\d/.test(msg);
156
160
  }
157
- function runGit(args2, cwd) {
158
- if (!RETRYABLE_GIT_OPS.has(args2[0])) return spawnGit(args2, cwd);
159
- return withRetry(() => spawnGit(args2, cwd), isTransientGitError, `[workspace] git ${args2[0]}`);
161
+ function runGit(args2, cwd, config2) {
162
+ if (!RETRYABLE_GIT_OPS.has(args2[0])) return spawnGit(args2, cwd, config2);
163
+ return withRetry(() => spawnGit(args2, cwd, config2), isTransientGitError, `[workspace] git ${args2[0]}`);
160
164
  }
161
165
  async function pruneIgnoredFromIndex(cwd) {
162
166
  try {
@@ -172,31 +176,18 @@ async function pruneIgnoredFromIndex(cwd) {
172
176
  function redactToken(text3) {
173
177
  return text3.replace(/x-access-token:[^@]+@/g, "x-access-token:***@");
174
178
  }
175
- function authUrl(repoFullName, token) {
176
- return `https://x-access-token:${token}@github.com/${repoFullName}.git`;
177
- }
178
179
  function cleanUrl(repoFullName) {
179
180
  return `https://github.com/${repoFullName}.git`;
180
181
  }
181
- async function pushBranch(cwd, repoFullName, token, branch) {
182
- await runGit(["remote", "set-url", "origin", authUrl(repoFullName, token)], cwd);
183
- try {
184
- await runGit(["push", "--force-with-lease", "-u", "origin", branch], cwd);
185
- } finally {
186
- await runGit(["remote", "set-url", "origin", cleanUrl(repoFullName)], cwd).catch(() => {
187
- });
188
- }
182
+ async function pushBranch(cwd, token, branch) {
183
+ await runGit(["push", "--force-with-lease", "-u", "origin", branch], cwd, authConfig(token));
189
184
  }
190
- async function fetchRef(baseDir, repoFullName, token, ref) {
191
- await runGit(["remote", "set-url", "origin", authUrl(repoFullName, token)], baseDir);
185
+ async function fetchRef(baseDir, token, ref) {
192
186
  try {
193
- await runGit(["fetch", "origin", ref], baseDir);
187
+ await runGit(["fetch", "origin", ref], baseDir, authConfig(token));
194
188
  return true;
195
189
  } catch {
196
190
  return false;
197
- } finally {
198
- await runGit(["remote", "set-url", "origin", cleanUrl(repoFullName)], baseDir).catch(() => {
199
- });
200
191
  }
201
192
  }
202
193
  var projectLocks = /* @__PURE__ */ new Map();
@@ -214,22 +205,15 @@ async function ensureBaseRepo(baseDir, repoFullName, defaultBranch, token) {
214
205
  () => {
215
206
  (0, import_node_fs2.rmSync)(baseDir, { recursive: true, force: true });
216
207
  (0, import_node_fs2.mkdirSync)(baseDir, { recursive: true });
217
- return spawnGit(["clone", "--branch", defaultBranch, authUrl(repoFullName, token), baseDir]);
208
+ return spawnGit(["clone", "--branch", defaultBranch, cleanUrl(repoFullName), baseDir], void 0, authConfig(token));
218
209
  },
219
210
  isTransientGitError,
220
211
  "[workspace] git clone"
221
212
  );
222
- await runGit(["remote", "set-url", "origin", cleanUrl(repoFullName)], baseDir);
223
213
  return;
224
214
  }
225
215
  console.log(`[workspace] Fetching ${repoFullName} (branch: ${defaultBranch})\u2026`);
226
- await runGit(["remote", "set-url", "origin", authUrl(repoFullName, token)], baseDir);
227
- try {
228
- await runGit(["fetch", "origin", defaultBranch], baseDir);
229
- } finally {
230
- await runGit(["remote", "set-url", "origin", cleanUrl(repoFullName)], baseDir).catch(() => {
231
- });
232
- }
216
+ await runGit(["fetch", "origin", defaultBranch], baseDir, authConfig(token));
233
217
  }
234
218
  async function prepareConversationWorkspace(projectId, conversationId, repoFullName, defaultBranch, token, opts = {}) {
235
219
  return withProjectLock(projectId, async () => {
@@ -242,7 +226,7 @@ async function prepareConversationWorkspace(projectId, conversationId, repoFullN
242
226
  const worktreeExists = (0, import_node_fs2.existsSync)((0, import_node_path2.join)(worktreeDir, ".git"));
243
227
  let convBranchFetched = false;
244
228
  if (restoringToCommit || !worktreeExists) {
245
- convBranchFetched = await fetchRef(baseDir, repoFullName, token, branchName);
229
+ convBranchFetched = await fetchRef(baseDir, token, branchName);
246
230
  }
247
231
  const assertReachable = async (cwd) => {
248
232
  if (!restoringToCommit) return;
@@ -689,14 +673,12 @@ async function materializeSession(opts) {
689
673
  return {
690
674
  sessionManager: opts.sdk.SessionManager.open(mode.sessionFilePath, sessionsDir(), opts.cwd),
691
675
  isContinue: true,
692
- sessionFilePath: mode.sessionFilePath,
693
676
  sessionKeyForSave: null
694
677
  };
695
678
  }
696
679
  return {
697
- sessionManager: opts.sdk.SessionManager.create(opts.cwd, sessionsDir(), { id: opts.createSessionId }),
680
+ sessionManager: opts.sdk.SessionManager.create(opts.cwd, sessionsDir(), { id: opts.sessionKey }),
698
681
  isContinue: false,
699
- sessionFilePath: null,
700
682
  sessionKeyForSave: opts.sessionKey
701
683
  };
702
684
  }
@@ -866,7 +848,6 @@ function ensureSkillGc(agentDir) {
866
848
  }
867
849
 
868
850
  // src/lib/streaming.ts
869
- var LOCAL_EXECUTED_TOOLS = /* @__PURE__ */ new Set(["read", "write", "edit", "bash", "grep", "find", "ls", "web_fetch", "client_shell", "list_todos", "create_todo", "update_todo"]);
870
851
  function formatToolInput(toolName, args2) {
871
852
  if (!args2) return "";
872
853
  switch (toolName) {
@@ -891,21 +872,31 @@ function formatToolInput(toolName, args2) {
891
872
  }
892
873
  }
893
874
  }
875
+ async function postRaw(serverUrl, path, body, token) {
876
+ const headers = { "Content-Type": "application/json" };
877
+ if (token) headers["Authorization"] = `Bearer ${token}`;
878
+ const res = await fetch(`${serverUrl}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
879
+ if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { httpStatus: res.status });
880
+ return await res.json();
881
+ }
894
882
  async function post(serverUrl, path, body, token) {
895
883
  try {
896
- const headers = { "Content-Type": "application/json" };
897
- if (token) headers["Authorization"] = `Bearer ${token}`;
898
- const res = await fetch(`${serverUrl}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
899
- if (!res.ok) {
900
- console.error(`[machine] POST ${path} \u2192 HTTP ${res.status}`);
901
- return {};
902
- }
903
- return await res.json();
884
+ return await postRaw(serverUrl, path, body, token);
904
885
  } catch (err) {
905
886
  console.error(`[machine] POST ${path} failed:`, err.message);
906
887
  return {};
907
888
  }
908
889
  }
890
+ async function postCritical(serverUrl, path, body, token) {
891
+ return withRetry(
892
+ () => postRaw(serverUrl, path, body, token),
893
+ (err) => {
894
+ const status = err.httpStatus;
895
+ return status === void 0 || status >= 500;
896
+ },
897
+ `[machine] POST ${path}`
898
+ );
899
+ }
909
900
  function startHeartbeat(serverUrl, stepId, agentId, onSignal, token) {
910
901
  let active = true;
911
902
  let inFlight = false;
@@ -926,7 +917,14 @@ function startHeartbeat(serverUrl, stepId, agentId, onSignal, token) {
926
917
  clearInterval(id);
927
918
  };
928
919
  }
929
- function attachStreamingSession(session, label, postToken) {
920
+ var POST_PACING_MS = 50;
921
+ function mergeKind(body) {
922
+ if (typeof body.text !== "string") return null;
923
+ if (body.type === void 0) return "text";
924
+ if (body.type === "thinking") return "thinking";
925
+ return null;
926
+ }
927
+ function attachStreamingSession(session, label, postToken, onStop) {
930
928
  const abort = new AbortController();
931
929
  abort.signal.addEventListener("abort", () => {
932
930
  try {
@@ -934,43 +932,37 @@ function attachStreamingSession(session, label, postToken) {
934
932
  } catch {
935
933
  }
936
934
  });
937
- let textBuffer = "";
938
- let thinkingBuffer = "";
939
935
  let fullText = "";
940
- let assistantStarted = false;
941
936
  let lastModelError;
942
- const startAssistant = () => {
943
- assistantStarted = true;
944
- textBuffer = "";
945
- thinkingBuffer = "";
946
- };
947
- const ensureAssistant = () => {
948
- if (!assistantStarted) startAssistant();
949
- };
950
- const emitToolStart = (toolName, detail) => {
951
- ensureAssistant();
952
- postToken({ type: "tool_start", toolName, detail });
937
+ const queue = [];
938
+ let pump = Promise.resolve();
939
+ let pumping = false;
940
+ const runPump = async () => {
941
+ try {
942
+ while (queue.length) {
943
+ const res = await postToken(queue.shift());
944
+ if (res.stop) onStop();
945
+ if (queue.length) await new Promise((r) => setTimeout(r, POST_PACING_MS));
946
+ }
947
+ } finally {
948
+ pumping = false;
949
+ }
953
950
  };
954
- const flush = () => {
955
- if (textBuffer) {
956
- const c = textBuffer;
957
- textBuffer = "";
958
- postToken({ text: c });
951
+ const push = (body) => {
952
+ const kind = mergeKind(body);
953
+ const last = queue[queue.length - 1];
954
+ if (kind && last && mergeKind(last) === kind) {
955
+ last.text = last.text + body.text;
956
+ return;
959
957
  }
960
- if (thinkingBuffer) {
961
- const c = thinkingBuffer;
962
- thinkingBuffer = "";
963
- postToken({ type: "thinking", text: c });
958
+ queue.push(body);
959
+ if (!pumping) {
960
+ pumping = true;
961
+ pump = runPump();
964
962
  }
965
963
  };
966
- const flushTimer = setInterval(flush, 50);
967
964
  const unsub = session.subscribe((event) => {
968
965
  const ev = event;
969
- if (ev.type === "message_start") {
970
- const message = ev.message;
971
- if (message?.role === "assistant") startAssistant();
972
- return;
973
- }
974
966
  if (ev.type === "message_end") {
975
967
  const message = ev.message;
976
968
  const stopReason = message?.stopReason;
@@ -989,33 +981,23 @@ function attachStreamingSession(session, label, postToken) {
989
981
  if (!ev.success) console.error(`[${label}] auto_retry_end failed finalError=${ev.finalError}`);
990
982
  return;
991
983
  }
992
- if (ev.type === "tool_execution_start") {
993
- const toolName = String(ev.toolName ?? "tool");
994
- const args2 = ev.args;
995
- emitToolStart(toolName, formatToolInput(toolName, args2));
996
- return;
997
- }
998
984
  if (ev.type === "message_update") {
999
985
  const ame = ev.assistantMessageEvent;
1000
986
  if (ame?.type === "text_delta") {
1001
987
  const delta = String(ame.delta ?? "");
1002
988
  if (delta) {
1003
- ensureAssistant();
1004
- textBuffer += delta;
1005
989
  fullText += delta;
990
+ push({ text: delta });
1006
991
  }
1007
992
  } else if (ame?.type === "thinking_delta") {
1008
993
  const delta = String(ame.delta ?? "");
1009
- if (delta) {
1010
- ensureAssistant();
1011
- thinkingBuffer += delta;
1012
- }
994
+ if (delta) push({ type: "thinking", text: delta });
1013
995
  } else if (ame?.type === "toolcall_end") {
1014
996
  const toolCall = ame.toolCall;
1015
997
  const toolName = String(toolCall?.name ?? "");
1016
- if (toolName && !LOCAL_EXECUTED_TOOLS.has(toolName)) {
998
+ if (toolName) {
1017
999
  const args2 = toolCall?.arguments;
1018
- emitToolStart(toolName, formatToolInput(toolName, args2));
1000
+ push({ type: "tool_start", toolName, detail: formatToolInput(toolName, args2) });
1019
1001
  }
1020
1002
  }
1021
1003
  }
@@ -1024,7 +1006,6 @@ function attachStreamingSession(session, label, postToken) {
1024
1006
  const dispose = () => {
1025
1007
  if (disposed) return;
1026
1008
  disposed = true;
1027
- clearInterval(flushTimer);
1028
1009
  unsub();
1029
1010
  try {
1030
1011
  session.dispose();
@@ -1033,16 +1014,17 @@ function attachStreamingSession(session, label, postToken) {
1033
1014
  };
1034
1015
  return {
1035
1016
  abort,
1036
- flush,
1037
1017
  getFullText: () => fullText,
1038
1018
  getLastModelError: () => lastModelError,
1019
+ idle: () => pump,
1039
1020
  dispose
1040
1021
  };
1041
1022
  }
1042
1023
 
1043
1024
  // src/machine.ts
1044
- function makeDone(serverUrl, stepId, agentId, conversationId, token) {
1045
- return (payload) => post(serverUrl, `/api/machine/done/${stepId}`, { agentId, conversationId, ...payload }, token);
1025
+ function makeDone(step, serverUrl, token) {
1026
+ const { stepId, conversationId, mode, agent: { agentId } } = step;
1027
+ return (payload) => postCritical(serverUrl, `/api/machine/done/${stepId}`, { agentId, conversationId, mode, ...payload }, token).catch((err) => console.error(`[step] ${stepId} done report failed:`, err.message));
1046
1028
  }
1047
1029
  async function computeDiffHash(cwd, defaultBranch, stepId) {
1048
1030
  try {
@@ -1166,18 +1148,19 @@ async function executeStep(step, serverUrl, token, tunnel) {
1166
1148
  console.log(`[step] ${stepId} start (conv ${conversationId})`);
1167
1149
  let interrupted = false;
1168
1150
  let abortRef = null;
1169
- const stopHeartbeat = startHeartbeat(serverUrl, stepId, agentId, () => {
1151
+ const stopTurn = () => {
1170
1152
  interrupted = true;
1171
1153
  abortRef?.abort();
1172
- }, token);
1173
- const done2 = makeDone(serverUrl, stepId, agentId, conversationId, token);
1154
+ };
1155
+ const stopHeartbeat = startHeartbeat(serverUrl, stepId, agentId, stopTurn, token);
1156
+ const done = makeDone(step, serverUrl, token);
1174
1157
  try {
1175
1158
  const runtime = await getPiRuntime();
1176
1159
  const { sdk, authStorage, modelRegistry, agentDir } = runtime;
1177
1160
  ensureSkillGc(agentDir);
1178
1161
  const model = resolveModel(modelRegistry, step.agent);
1179
1162
  if (!model) {
1180
- await done2({ error: "No authenticated model available on this machine. Run `tds provider add` (or `tds provider login <name>`) on the machine." });
1163
+ await done({ error: "No authenticated model available on this machine. Run `tds provider add` (or `tds provider login <name>`) on the machine." });
1181
1164
  return;
1182
1165
  }
1183
1166
  console.log(`[step] ${stepId} using model ${model.provider}/${model.id}`);
@@ -1185,23 +1168,22 @@ async function executeStep(step, serverUrl, token, tunnel) {
1185
1168
  try {
1186
1169
  cwd = await prepareStepWorkspace(step) ?? agentDir;
1187
1170
  } catch (err) {
1188
- await done2({ error: `Workspace preparation failed: ${err.message}` });
1171
+ await done({ error: `Workspace preparation failed: ${err.message}` });
1189
1172
  return;
1190
1173
  }
1191
1174
  if (step.restore) {
1192
1175
  try {
1193
1176
  await restoreSessionToCheckpoint(step);
1194
1177
  } catch (err) {
1195
- await done2({ error: `Session rewind failed: ${err.message}` });
1178
+ await done({ error: `Session rewind failed: ${err.message}` });
1196
1179
  return;
1197
1180
  }
1198
1181
  }
1199
1182
  const ephemeral = step.mode === "ephemeral_turn";
1200
- const m = ephemeral ? { sessionManager: sdk.SessionManager.inMemory(cwd), isContinue: false, sessionFilePath: null, sessionKeyForSave: null } : await materializeSession({
1183
+ const m = ephemeral ? { sessionManager: sdk.SessionManager.inMemory(cwd), isContinue: false, sessionKeyForSave: null } : await materializeSession({
1201
1184
  sdk,
1202
1185
  sessionKey: conversationId,
1203
1186
  cwd,
1204
- createSessionId: conversationId,
1205
1187
  restoreUrl: step.sessionRestoreUrl
1206
1188
  });
1207
1189
  console.log(m.isContinue ? `[step] ${stepId} continue session ${conversationId}` : `[step] ${stepId} new session ${conversationId}`);
@@ -1226,62 +1208,14 @@ async function executeStep(step, serverUrl, token, tunnel) {
1226
1208
  if (m.sessionKeyForSave && session.sessionFile) {
1227
1209
  saveSessionPath(m.sessionKeyForSave, session.sessionFile);
1228
1210
  }
1229
- let flushChain = Promise.resolve();
1230
- let mergeableTextTail = null;
1231
- let mergeableThinkingTail = null;
1232
- const enqueueTokenPost = (body) => {
1233
- const isPlainText = typeof body.text === "string" && body.type === void 0;
1234
- const isThinking = body.type === "thinking" && typeof body.text === "string";
1235
- if (isPlainText) {
1236
- mergeableThinkingTail = null;
1237
- if (mergeableTextTail) {
1238
- mergeableTextTail.text += body.text;
1239
- return;
1240
- }
1241
- const tail = { text: body.text };
1242
- mergeableTextTail = tail;
1243
- flushChain = flushChain.then(async () => {
1244
- mergeableTextTail = null;
1245
- const res = await post(serverUrl, `/api/machine/token/${stepId}`, { agentId, text: tail.text }, token);
1246
- if (res.stop && !interrupted) {
1247
- interrupted = true;
1248
- abortRef?.abort();
1249
- }
1250
- });
1251
- return;
1252
- }
1253
- if (isThinking) {
1254
- mergeableTextTail = null;
1255
- if (mergeableThinkingTail) {
1256
- mergeableThinkingTail.text += body.text;
1257
- return;
1258
- }
1259
- const tail = { text: body.text };
1260
- mergeableThinkingTail = tail;
1261
- flushChain = flushChain.then(async () => {
1262
- mergeableThinkingTail = null;
1263
- const res = await post(serverUrl, `/api/machine/token/${stepId}`, { agentId, type: "thinking", text: tail.text }, token);
1264
- if (res.stop && !interrupted) {
1265
- interrupted = true;
1266
- abortRef?.abort();
1267
- }
1268
- });
1269
- return;
1270
- }
1271
- mergeableTextTail = null;
1272
- mergeableThinkingTail = null;
1273
- flushChain = flushChain.then(async () => {
1274
- const res = await post(serverUrl, `/api/machine/token/${stepId}`, { agentId, ...body }, token);
1275
- if (res.stop && !interrupted) {
1276
- interrupted = true;
1277
- abortRef?.abort();
1278
- }
1279
- });
1280
- };
1281
- const stream = attachStreamingSession(session, `step:${stepId}`, enqueueTokenPost);
1211
+ const stream = attachStreamingSession(
1212
+ session,
1213
+ `step:${stepId}`,
1214
+ (body) => post(serverUrl, `/api/machine/token/${stepId}`, { agentId, ...body }, token),
1215
+ stopTurn
1216
+ );
1282
1217
  abortRef = stream.abort;
1283
1218
  if (interrupted) stream.abort.abort();
1284
- const fullPrompt = step.prompt;
1285
1219
  let commitSha;
1286
1220
  let sessionLeafEntryId;
1287
1221
  let diffHash;
@@ -1297,10 +1231,10 @@ async function executeStep(step, serverUrl, token, tunnel) {
1297
1231
  } catch (err) {
1298
1232
  console.warn(`[step] ${stepId} checkpoint commit failed:`, err.message);
1299
1233
  }
1300
- const { repoFullName, installationToken, defaultBranch } = step.workspace;
1234
+ const { installationToken, defaultBranch } = step.workspace;
1301
1235
  const branchName = `tds/conv-${conversationId}`;
1302
1236
  try {
1303
- await pushBranch(cwd, repoFullName, installationToken, branchName);
1237
+ await pushBranch(cwd, installationToken, branchName);
1304
1238
  console.log(`[step] ${stepId} pushed ${branchName}`);
1305
1239
  } catch (err) {
1306
1240
  console.error(`[step] ${stepId} push failed:`, err.message);
@@ -1314,31 +1248,28 @@ async function executeStep(step, serverUrl, token, tunnel) {
1314
1248
  const donePayload = () => ({ text: stream.getFullText(), modelId: step.agent.modelId || model.id, commitSha, sessionLeafEntryId, diffHash });
1315
1249
  const images = await downloadStepImages(step);
1316
1250
  try {
1317
- await session.prompt(fullPrompt, images.length ? { images } : void 0);
1318
- stream.dispose();
1319
- stream.flush();
1320
- await flushChain;
1251
+ await session.prompt(step.prompt, images.length ? { images } : void 0);
1252
+ await stream.idle();
1321
1253
  const modelError = !stream.getFullText() && stream.getLastModelError();
1322
1254
  if (!ephemeral) await backupSession(conversationId, session.sessionFile, step.sessionBackupUrl);
1323
1255
  if (modelError) {
1324
- await done2({ error: modelError });
1256
+ await done({ error: modelError });
1325
1257
  } else {
1326
1258
  await captureCheckpoint();
1327
- await done2(donePayload());
1259
+ await done(donePayload());
1328
1260
  }
1329
1261
  } catch (err) {
1330
- stream.dispose();
1331
- await flushChain.catch(() => {
1262
+ await stream.idle().catch(() => {
1332
1263
  });
1333
1264
  if (!ephemeral) await backupSession(conversationId, session.sessionFile, step.sessionBackupUrl);
1334
1265
  if (interrupted) {
1335
1266
  console.log(`[step] ${stepId} interrupted \u2014 saving partial content`);
1336
1267
  await captureCheckpoint();
1337
- await done2(donePayload());
1268
+ await done(donePayload());
1338
1269
  } else {
1339
1270
  const msg = err instanceof Error ? err.message : String(err);
1340
1271
  console.error(`[step] ${stepId} failed:`, msg);
1341
- await done2({ error: msg });
1272
+ await done({ error: msg });
1342
1273
  }
1343
1274
  } finally {
1344
1275
  stream.dispose();
@@ -1348,15 +1279,15 @@ async function executeStep(step, serverUrl, token, tunnel) {
1348
1279
  }
1349
1280
  }
1350
1281
  async function executeRestore(step, serverUrl, token) {
1351
- const { stepId, conversationId, agent: { agentId } } = step;
1282
+ const { stepId, conversationId } = step;
1352
1283
  console.log(`[restore] ${stepId} start (conv ${conversationId})`);
1353
- const done2 = makeDone(serverUrl, stepId, agentId, conversationId, token);
1284
+ const done = makeDone(step, serverUrl, token);
1354
1285
  let diffHash;
1355
1286
  const rewindCode = async () => {
1356
1287
  const cwd = await prepareStepWorkspace(step);
1357
1288
  if (!cwd || !step.workspace) return;
1358
- const { repoFullName, defaultBranch, installationToken } = step.workspace;
1359
- await pushBranch(cwd, repoFullName, installationToken, `tds/conv-${conversationId}`);
1289
+ const { defaultBranch, installationToken } = step.workspace;
1290
+ await pushBranch(cwd, installationToken, `tds/conv-${conversationId}`);
1360
1291
  diffHash = await computeDiffHash(cwd, defaultBranch, stepId);
1361
1292
  };
1362
1293
  const rewindSession = async () => {
@@ -1368,10 +1299,10 @@ async function executeRestore(step, serverUrl, token) {
1368
1299
  if (failed) {
1369
1300
  const msg = failed.reason instanceof Error ? failed.reason.message : String(failed.reason);
1370
1301
  console.error(`[restore] ${stepId} failed:`, msg);
1371
- await done2({ error: msg });
1302
+ await done({ error: msg });
1372
1303
  return;
1373
1304
  }
1374
- await done2({ diffHash });
1305
+ await done({ diffHash });
1375
1306
  console.log(`[restore] ${stepId} done`);
1376
1307
  }
1377
1308
 
@@ -16067,13 +15998,8 @@ var STEP_DEF = {
16067
15998
  plan_review: { track: "plan", sink: "review" },
16068
15999
  implement_review: { track: "implement", sink: "review" }
16069
16000
  };
16070
- var STEP_PHASE = {
16071
- plan: STEP_DEF.plan.phase,
16072
- plan_revision: STEP_DEF.plan_revision.phase,
16073
- implement: STEP_DEF.implement.phase,
16074
- implement_revision: STEP_DEF.implement_revision.phase,
16075
- merge: STEP_DEF.merge.phase
16076
- };
16001
+ var isReviewStep = (k) => STEP_DEF[k].sink === "review";
16002
+ var REVIEW_STEP_KINDS = Object.keys(STEP_DEF).filter(isReviewStep);
16077
16003
  var CreateTeamRequestSchema = external_exports.object({
16078
16004
  name: external_exports.string().trim().min(1)
16079
16005
  });
@@ -16083,7 +16009,8 @@ var UpdateTeamRequestSchema = external_exports.object({
16083
16009
  });
16084
16010
  var UpdateProjectRequestSchema = external_exports.object({
16085
16011
  name: external_exports.string().min(1).optional(),
16086
- description: external_exports.string().optional()
16012
+ description: external_exports.string().optional(),
16013
+ defaultBranch: external_exports.string().min(1).optional()
16087
16014
  });
16088
16015
  var UpdateUserProfileRequestSchema = external_exports.object({
16089
16016
  name: external_exports.string().trim().optional(),
@@ -16124,7 +16051,10 @@ var CreateTodoRequestSchema = external_exports.object({
16124
16051
  // Spec is optional: a title-only todo is allowed (the UI flags the missing spec).
16125
16052
  spec: external_exports.string().trim().default(""),
16126
16053
  tagIds: external_exports.array(external_exports.string()).optional(),
16127
- orderIndex: external_exports.number().optional()
16054
+ orderIndex: external_exports.number().optional(),
16055
+ // Birth phase — idle states only (a build is still a separate, explicit action). Defaults to
16056
+ // 'todo'; 'idea' lets a duplicate of a parked idea land straight in the idea library.
16057
+ phase: external_exports.enum(["todo", "idea"]).optional()
16128
16058
  });
16129
16059
  var AgentEntrySchema = external_exports.object({
16130
16060
  agentId: external_exports.string(),
@@ -16369,8 +16299,8 @@ ${node.sshHostKey}` : null };
16369
16299
  if (Array.isArray(data?.roles)) {
16370
16300
  this.setRoles(data.roles.filter((r) => r === "builder" || r === "runner"));
16371
16301
  }
16302
+ this.lastSentTunnelKey = key;
16372
16303
  }
16373
- this.lastSentTunnelKey = key;
16374
16304
  } catch {
16375
16305
  }
16376
16306
  }
@@ -16397,9 +16327,7 @@ ${node.sshHostKey}` : null };
16397
16327
  try {
16398
16328
  while (this.running && this.canClaim) {
16399
16329
  try {
16400
- if (this.maxConcurrentTasks == null) await this.reportPresence();
16401
- const maxConcurrentTasks = this.maxConcurrentTasks;
16402
- if (maxConcurrentTasks == null) throw new Error("Missing server maxConcurrentTasks");
16330
+ const maxConcurrentTasks = this.maxConcurrentTasks ?? 1;
16403
16331
  if (this.runningTasks.size >= maxConcurrentTasks) {
16404
16332
  await Promise.race([...this.runningTasks.values(), sleep(2e3, this.abortController.signal)]);
16405
16333
  continue;
@@ -16500,6 +16428,7 @@ function resolveBin(name) {
16500
16428
  dir = parent;
16501
16429
  }
16502
16430
  }
16431
+ console.warn(`[bin] ${name} binary not found (no @todos-dev/cli-${process.platform}-${process.arch} package, no vendor/ tree near ${__dirname}) \u2014 falling back to PATH. Run \`pnpm vendor:bins\` (dev) or reinstall the CLI.`);
16503
16432
  return name;
16504
16433
  }
16505
16434
  var TUNNEL_BIN = resolveBin("tds-tunnel");
@@ -16533,7 +16462,7 @@ function spawnMirror(opts) {
16533
16462
  opts.serverUrl,
16534
16463
  "--token",
16535
16464
  opts.token,
16536
- "--conversation",
16465
+ "--build",
16537
16466
  opts.buildId,
16538
16467
  "--dest",
16539
16468
  opts.dest,
@@ -16619,6 +16548,7 @@ var SyncManager = class {
16619
16548
  if (!this.running) return;
16620
16549
  const resolved = await this.fetchMounts();
16621
16550
  if (resolved === null) return;
16551
+ if (!this.running) return;
16622
16552
  const desired = new Map(resolved.map((r) => [r.mountId, r]));
16623
16553
  for (const [mountId, t] of [...this.tunnels]) {
16624
16554
  const want = desired.get(mountId);
@@ -16776,6 +16706,9 @@ async function syncModelsToServer(serverUrl, token, runtime) {
16776
16706
  // src/lib/tunnel.ts
16777
16707
  var import_node_child_process3 = require("node:child_process");
16778
16708
  var SHELL_BUFFER_CAP = 256 * 1024;
16709
+ var RESTART_MIN_MS = 1e3;
16710
+ var RESTART_MAX_MS = 6e4;
16711
+ var EMPTY_BUFFER = Buffer.alloc(0);
16779
16712
  var TunnelServer = class _TunnelServer {
16780
16713
  constructor(serverUrl, token) {
16781
16714
  this.serverUrl = serverUrl;
@@ -16784,14 +16717,13 @@ var TunnelServer = class _TunnelServer {
16784
16717
  proc = null;
16785
16718
  node = null;
16786
16719
  onChange = null;
16787
- // Reverse-shell subscribers, keyed by (build, machine) — a build mirrored onto several runners has
16788
- // one shell per machine. The agent's client_shell tool subscribes while it drives one; sidecar
16789
- // shell-output/shell-closed events fan out to them.
16790
- shellListeners = /* @__PURE__ */ new Map();
16791
- // Buffered PTY output per (build, machine), drained by the tool. Recorded even with no live
16792
- // subscriber so output a backgrounded command emits between tool calls survives until the next drain.
16793
- shellBuffers = /* @__PURE__ */ new Map();
16794
- closedShells = /* @__PURE__ */ new Set();
16720
+ stopped = false;
16721
+ restartTimer = null;
16722
+ restartDelayMs = RESTART_MIN_MS;
16723
+ // Reverse shells, keyed by (build, machine) — a build mirrored onto several runners has one shell
16724
+ // per machine. Output is buffered even with no live subscriber so output a backgrounded command
16725
+ // emits between tool calls survives until the next drain.
16726
+ shells = /* @__PURE__ */ new Map();
16795
16727
  // Set the single handler fired whenever the tunnel node appears/changes/clears, so presence can
16796
16728
  // be pushed immediately instead of waiting for the next heartbeat tick. Fires now if already ready.
16797
16729
  setNodeChangeHandler(cb) {
@@ -16799,6 +16731,10 @@ var TunnelServer = class _TunnelServer {
16799
16731
  if (this.node) cb();
16800
16732
  }
16801
16733
  start() {
16734
+ this.stopped = false;
16735
+ this.spawnSidecar();
16736
+ }
16737
+ spawnSidecar() {
16802
16738
  try {
16803
16739
  this.proc = (0, import_node_child_process3.spawn)(
16804
16740
  TUNNEL_BIN,
@@ -16819,11 +16755,22 @@ var TunnelServer = class _TunnelServer {
16819
16755
  this.node = null;
16820
16756
  this.closeAllShells();
16821
16757
  this.onChange?.();
16758
+ this.scheduleRestart();
16822
16759
  });
16823
16760
  this.proc.stdout?.on("data", (d) => this.onStdout(String(d)));
16824
16761
  this.proc.stdin?.on("error", () => {
16825
16762
  });
16826
16763
  }
16764
+ scheduleRestart() {
16765
+ if (this.stopped || this.restartTimer) return;
16766
+ const delay2 = this.restartDelayMs;
16767
+ this.restartDelayMs = Math.min(this.restartDelayMs * 2, RESTART_MAX_MS);
16768
+ console.warn(`[tunnel] sidecar exited \u2014 restarting in ${Math.round(delay2 / 1e3)}s`);
16769
+ this.restartTimer = setTimeout(() => {
16770
+ this.restartTimer = null;
16771
+ this.spawnSidecar();
16772
+ }, delay2);
16773
+ }
16827
16774
  // Reverse-shell map key: a build mirrored onto several runners has one shell per machine.
16828
16775
  static shellKey(build, machine) {
16829
16776
  return `${build}\0${machine}`;
@@ -16833,19 +16780,22 @@ var TunnelServer = class _TunnelServer {
16833
16780
  onStdout = createNdjsonParser((evt) => {
16834
16781
  if (evt.event === "ready" && evt.tailnetAddr && evt.sshHostKey) {
16835
16782
  this.node = { tailnetAddr: evt.tailnetAddr, sshHostKey: evt.sshHostKey };
16783
+ this.restartDelayMs = RESTART_MIN_MS;
16836
16784
  console.log(`[tunnel] ready (tailnet ${evt.tailnetAddr})`);
16837
16785
  this.onChange?.();
16838
16786
  } else if ((evt.event === "shell-opened" || evt.event === "shell-output" || evt.event === "shell-closed") && evt.build) {
16839
- const key = _TunnelServer.shellKey(evt.build, evt.machine ?? "");
16787
+ const s = this.shellState(_TunnelServer.shellKey(evt.build, evt.machine ?? ""));
16840
16788
  if (evt.event === "shell-opened") {
16841
- this.closedShells.delete(key);
16789
+ s.closed = false;
16842
16790
  } else if (evt.event === "shell-output") {
16843
- this.appendShellBuffer(key, Buffer.from(evt.data ?? "", "base64"));
16844
- for (const l of this.shellListeners.get(key) ?? []) l.onActivity();
16791
+ s.closed = false;
16792
+ let next = Buffer.concat([s.buffer, Buffer.from(evt.data ?? "", "base64")]);
16793
+ if (next.length > SHELL_BUFFER_CAP) next = next.subarray(next.length - SHELL_BUFFER_CAP);
16794
+ s.buffer = next;
16795
+ for (const l of s.listeners) l.onActivity();
16845
16796
  } else {
16846
- this.closedShells.add(key);
16847
- const subs = [...this.shellListeners.get(key) ?? []];
16848
- for (const l of subs) l.onClosed();
16797
+ s.closed = true;
16798
+ for (const l of [...s.listeners]) l.onClosed();
16849
16799
  }
16850
16800
  } else if (evt.event === "error") {
16851
16801
  console.warn(`[tunnel] ${evt.message ?? "error"}`);
@@ -16873,51 +16823,50 @@ var TunnelServer = class _TunnelServer {
16873
16823
  return this.writeControl({ cmd: "shell-input", build, machine, data: Buffer.from(data, "utf8").toString("base64") });
16874
16824
  }
16875
16825
  shellClosed(build, machine) {
16876
- return this.closedShells.has(_TunnelServer.shellKey(build, machine));
16826
+ return this.shells.get(_TunnelServer.shellKey(build, machine))?.closed ?? false;
16877
16827
  }
16878
16828
  subscribeShell(build, machine, l) {
16879
- const key = _TunnelServer.shellKey(build, machine);
16880
- let set2 = this.shellListeners.get(key);
16881
- if (!set2) {
16882
- set2 = /* @__PURE__ */ new Set();
16883
- this.shellListeners.set(key, set2);
16884
- }
16885
- set2.add(l);
16829
+ const s = this.shellState(_TunnelServer.shellKey(build, machine));
16830
+ s.listeners.add(l);
16886
16831
  return () => {
16887
- const s = this.shellListeners.get(key);
16888
- if (!s) return;
16889
- s.delete(l);
16890
- if (s.size === 0) this.shellListeners.delete(key);
16832
+ s.listeners.delete(l);
16891
16833
  };
16892
16834
  }
16893
16835
  // Return and clear a (build, machine) shell's buffered PTY output (UTF-8). The tool calls this
16894
- // after a quiet period to get everything the shell printed since the last drain.
16836
+ // after a quiet period to get everything the shell printed since the last drain; the closed
16837
+ // marker is cleared too so the next session for this key starts fresh.
16895
16838
  drainShell(build, machine) {
16896
16839
  const key = _TunnelServer.shellKey(build, machine);
16897
- const buf = this.shellBuffers.get(key);
16898
- this.closedShells.delete(key);
16899
- if (!buf || buf.length === 0) return "";
16900
- this.shellBuffers.delete(key);
16901
- return buf.toString("utf8");
16902
- }
16903
- appendShellBuffer(key, chunk) {
16904
- this.closedShells.delete(key);
16905
- const prev = this.shellBuffers.get(key);
16906
- let next = prev ? Buffer.concat([prev, chunk]) : chunk;
16907
- if (next.length > SHELL_BUFFER_CAP) next = next.subarray(next.length - SHELL_BUFFER_CAP);
16908
- this.shellBuffers.set(key, next);
16840
+ const s = this.shells.get(key);
16841
+ if (!s) return "";
16842
+ const out = s.buffer.toString("utf8");
16843
+ s.buffer = EMPTY_BUFFER;
16844
+ s.closed = false;
16845
+ if (s.listeners.size === 0) this.shells.delete(key);
16846
+ return out;
16847
+ }
16848
+ shellState(key) {
16849
+ let s = this.shells.get(key);
16850
+ if (!s) {
16851
+ s = { buffer: EMPTY_BUFFER, closed: false, listeners: /* @__PURE__ */ new Set() };
16852
+ this.shells.set(key, s);
16853
+ }
16854
+ return s;
16909
16855
  }
16910
16856
  closeAllShells() {
16911
- const all = [...this.shellListeners.values()].flatMap((s) => [...s]);
16912
- this.shellListeners.clear();
16913
- for (const l of all) l.onClosed();
16914
- this.closedShells.clear();
16915
- this.shellBuffers.clear();
16857
+ const states = [...this.shells.values()];
16858
+ this.shells.clear();
16859
+ for (const s of states) for (const l of [...s.listeners]) l.onClosed();
16916
16860
  }
16917
16861
  current() {
16918
16862
  return this.node;
16919
16863
  }
16920
16864
  stop() {
16865
+ this.stopped = true;
16866
+ if (this.restartTimer) {
16867
+ clearTimeout(this.restartTimer);
16868
+ this.restartTimer = null;
16869
+ }
16921
16870
  this.proc?.kill();
16922
16871
  this.proc = null;
16923
16872
  this.node = null;
@@ -16927,7 +16876,7 @@ var TunnelServer = class _TunnelServer {
16927
16876
 
16928
16877
  // src/lib/flagParser.ts
16929
16878
  var REPEATABLE = /* @__PURE__ */ new Set(["model"]);
16930
- function parseFlags(args2) {
16879
+ function parseFlags(args2, shortFlags = {}) {
16931
16880
  const flags = {};
16932
16881
  const repeated = {};
16933
16882
  const bools = {};
@@ -16935,12 +16884,9 @@ function parseFlags(args2) {
16935
16884
  for (let i = 0; i < args2.length; i++) {
16936
16885
  const a = args2[i];
16937
16886
  if (!a.startsWith("--")) {
16938
- if (a === "-y") {
16939
- bools["yes"] = true;
16940
- continue;
16941
- }
16942
- if (a === "-f") {
16943
- bools["foreground"] = true;
16887
+ const short = a.startsWith("-") ? shortFlags[a.slice(1)] : void 0;
16888
+ if (short) {
16889
+ bools[short] = true;
16944
16890
  continue;
16945
16891
  }
16946
16892
  positionals.push(a);
@@ -16979,7 +16925,7 @@ function assign(key, value, flags, repeated) {
16979
16925
 
16980
16926
  // src/commands/start.ts
16981
16927
  async function startCommand(args2) {
16982
- const { flags, bools } = parseFlags(args2);
16928
+ const { flags, bools } = parseFlags(args2, { f: "foreground" });
16983
16929
  if (bools.supervisor) return runSupervisor(args2);
16984
16930
  if (bools.foreground) {
16985
16931
  const { config: config2, serverUrl } = await ensureEnrolled(flags);
@@ -17182,13 +17128,13 @@ async function restartCommand(args2) {
17182
17128
  var import_child_process4 = require("child_process");
17183
17129
  var import_fs5 = require("fs");
17184
17130
  async function logsCommand(args2) {
17185
- const { bools } = parseFlags(args2);
17131
+ const { bools } = parseFlags(args2, { f: "follow" });
17186
17132
  const p = logPath();
17187
17133
  if (!(0, import_fs5.existsSync)(p)) {
17188
17134
  console.log(`[tds] No log file yet (${p}). Run 'tds start' first.`);
17189
17135
  return;
17190
17136
  }
17191
- const follow = bools.foreground || bools.follow;
17137
+ const follow = bools.follow;
17192
17138
  console.error(`[tds] ${p}`);
17193
17139
  const tailArgs = follow ? ["-f", p] : ["-n", "200", p];
17194
17140
  const child = (0, import_child_process4.spawn)("tail", tailArgs, { stdio: ["ignore", "inherit", "inherit"] });
@@ -17245,7 +17191,7 @@ function renderTabBar(tabs, active) {
17245
17191
  return i === active ? import_yoctocolors_cjs.default.inverse(import_yoctocolors_cjs.default.cyan(label)) : import_yoctocolors_cjs.default.dim(label);
17246
17192
  }).join(" ");
17247
17193
  }
17248
- var tabbedSearchPrompt = (0, import_core8.createPrompt)((config2, done2) => {
17194
+ var tabbedSearchPrompt = (0, import_core8.createPrompt)((config2, done) => {
17249
17195
  const { tabs } = config2;
17250
17196
  const pageSize = config2.pageSize ?? 8;
17251
17197
  const theme = (0, import_core8.makeTheme)({});
@@ -17266,7 +17212,7 @@ var tabbedSearchPrompt = (0, import_core8.createPrompt)((config2, done2) => {
17266
17212
  if ((0, import_core8.isEnterKey)(key)) {
17267
17213
  if (!selected) return;
17268
17214
  setStatus("done");
17269
- done2(selected.value);
17215
+ done(selected.value);
17270
17216
  return;
17271
17217
  }
17272
17218
  if (key.name === "left" || key.name === "right") {
@@ -17556,7 +17502,7 @@ async function menu() {
17556
17502
  }
17557
17503
  }
17558
17504
  async function addProvider(args2) {
17559
- const parsed = parseFlags(args2);
17505
+ const parsed = parseFlags(args2, { y: "yes" });
17560
17506
  const yes = parsed.bools["yes"] === true;
17561
17507
  const runtime = await getPiRuntime();
17562
17508
  const presets = buildPresets(runtime.builtinProviderIds, runtime.displayNameFor);
@@ -17703,7 +17649,7 @@ async function listProviders(args2) {
17703
17649
  }
17704
17650
  }
17705
17651
  async function removeProvider2(args2) {
17706
- const { positionals, bools } = parseFlags(args2);
17652
+ const { positionals, bools } = parseFlags(args2, { y: "yes" });
17707
17653
  const yes = bools["yes"] === true;
17708
17654
  const runtime = await getPiRuntime();
17709
17655
  const { authStorage } = runtime;
@@ -17808,6 +17754,8 @@ async function logoutCommand() {
17808
17754
  // src/index.ts
17809
17755
  var COMMANDS = {
17810
17756
  start: {
17757
+ run: startCommand,
17758
+ longLived: true,
17811
17759
  summary: "Bring this machine online in the background (enroll if needed)",
17812
17760
  help: `Usage: tds start [options]
17813
17761
 
@@ -17831,6 +17779,7 @@ Options:
17831
17779
  --server <url> Override the server URL`
17832
17780
  },
17833
17781
  stop: {
17782
+ run: () => stopCommand(),
17834
17783
  summary: "Stop the background machine",
17835
17784
  help: `Usage: tds stop
17836
17785
 
@@ -17838,6 +17787,8 @@ Stop the background daemon started by 'tds start'. Sends SIGTERM so in-flight ta
17838
17787
  shut down cleanly; SIGKILLs only if it doesn't exit in time.`
17839
17788
  },
17840
17789
  restart: {
17790
+ run: restartCommand,
17791
+ longLived: true,
17841
17792
  summary: "Restart the background machine",
17842
17793
  help: `Usage: tds restart [options]
17843
17794
 
@@ -17845,6 +17796,7 @@ Stop the background daemon (if running) and start it again in the background. Ta
17845
17796
  same options as 'tds start'.`
17846
17797
  },
17847
17798
  logs: {
17799
+ run: logsCommand,
17848
17800
  summary: "Show the background machine's logs",
17849
17801
  help: `Usage: tds logs [-f]
17850
17802
 
@@ -17854,6 +17806,7 @@ Options:
17854
17806
  -f, --follow Follow the log as it grows (like 'tail -f')`
17855
17807
  },
17856
17808
  logout: {
17809
+ run: () => logoutCommand(),
17857
17810
  summary: "Forget this machine's local registration",
17858
17811
  help: `Usage: tds logout
17859
17812
 
@@ -17862,12 +17815,14 @@ Stop the background daemon (if running), then clear ~/.tds/machine.json so the n
17862
17815
  (an admin does that in the web UI).`
17863
17816
  },
17864
17817
  status: {
17818
+ run: () => statusCommand(),
17865
17819
  summary: "Show this machine's registration and status",
17866
17820
  help: `Usage: tds status
17867
17821
 
17868
17822
  Show this machine's registration and current status.`
17869
17823
  },
17870
17824
  provider: {
17825
+ run: providerCommand,
17871
17826
  summary: "Manage AI providers and models",
17872
17827
  help: `Usage: tds provider <add|list|remove|login> [options]
17873
17828
 
@@ -17896,38 +17851,14 @@ if (COMMANDS[cmd] && (rest.includes("--help") || rest.includes("-h"))) {
17896
17851
  console.log(COMMANDS[cmd].help);
17897
17852
  process.exit(0);
17898
17853
  }
17899
- switch (cmd) {
17900
- // One-shot commands: exit explicitly when done (fetch keep-alive sockets block natural exit).
17901
- case "logout":
17902
- logoutCommand().then(done).catch(fatal);
17903
- break;
17904
- case "status":
17905
- statusCommand().then(done).catch(fatal);
17906
- break;
17907
- case "provider":
17908
- providerCommand(rest).then(done).catch(fatal);
17909
- break;
17910
- case "stop":
17911
- stopCommand().then(done).catch(fatal);
17912
- break;
17913
- case "logs":
17914
- logsCommand(rest).then(done).catch(fatal);
17915
- break;
17916
- // start/restart self-detach (exit after launching) or run long-lived (--foreground/--supervisor).
17917
- case "start":
17918
- startCommand(rest).catch(fatal);
17919
- break;
17920
- case "restart":
17921
- restartCommand(rest).catch(fatal);
17922
- break;
17923
- default:
17924
- console.error(`[tds] Unknown command: ${cmd}`);
17925
- printHelp();
17926
- process.exit(1);
17927
- }
17928
- function done() {
17929
- process.exit(0);
17854
+ var command = COMMANDS[cmd];
17855
+ if (!command) {
17856
+ console.error(`[tds] Unknown command: ${cmd}`);
17857
+ printHelp();
17858
+ process.exit(1);
17930
17859
  }
17860
+ if (command.longLived) command.run(rest).catch(fatal);
17861
+ else command.run(rest).then(() => process.exit(0)).catch(fatal);
17931
17862
  function fatal(err) {
17932
17863
  console.error("[tds] Fatal:", err.message);
17933
17864
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@todos-dev/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "bin": {
5
5
  "tds": "dist/index.js"
6
6
  },
@@ -11,8 +11,8 @@
11
11
  "access": "public"
12
12
  },
13
13
  "dependencies": {
14
- "@earendil-works/pi-ai": "0.80.2",
15
- "@earendil-works/pi-coding-agent": "0.80.2",
14
+ "@earendil-works/pi-ai": "0.80.3",
15
+ "@earendil-works/pi-coding-agent": "0.80.3",
16
16
  "@inquirer/core": "^10",
17
17
  "@inquirer/prompts": "^7",
18
18
  "@inquirer/search": "^3",
@@ -27,12 +27,12 @@
27
27
  "@tds/types": "0.1.0"
28
28
  },
29
29
  "optionalDependencies": {
30
- "@todos-dev/cli-darwin-arm64": "0.1.1",
31
- "@todos-dev/cli-darwin-x64": "0.1.1",
32
- "@todos-dev/cli-linux-x64": "0.1.1",
33
- "@todos-dev/cli-linux-arm64": "0.1.1",
34
- "@todos-dev/cli-win32-x64": "0.1.1",
35
- "@todos-dev/cli-win32-arm64": "0.1.1"
30
+ "@todos-dev/cli-darwin-arm64": "0.1.2",
31
+ "@todos-dev/cli-darwin-x64": "0.1.2",
32
+ "@todos-dev/cli-linux-x64": "0.1.2",
33
+ "@todos-dev/cli-linux-arm64": "0.1.2",
34
+ "@todos-dev/cli-win32-x64": "0.1.2",
35
+ "@todos-dev/cli-win32-arm64": "0.1.2"
36
36
  },
37
37
  "scripts": {
38
38
  "dev": "tsx watch src/index.ts",
@@ -41,6 +41,6 @@
41
41
  "typecheck": "tsc --noEmit",
42
42
  "release": "node ../../scripts/release-cli.mjs",
43
43
  "start": "node dist/index.js",
44
- "reinstall": "rm -f todos-dev-cli-*.tgz && pnpm pack && npm i -g \"$PWD/$(ls todos-dev-cli-*.tgz)\" && rm -f todos-dev-cli-*.tgz"
44
+ "reinstall": "rm -f todos-dev-cli-*.tgz && TDS_BUNDLE_TUNNEL=1 pnpm pack && npm i -g \"$PWD/$(ls todos-dev-cli-*.tgz)\" && rm -f todos-dev-cli-*.tgz"
45
45
  }
46
46
  }