@todos-dev/cli 0.1.0 → 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.
- package/dist/index.js +216 -285
- 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
|
|
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,
|
|
182
|
-
await runGit(["
|
|
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,
|
|
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,
|
|
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(["
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
|
998
|
+
if (toolName) {
|
|
1017
999
|
const args2 = toolCall?.arguments;
|
|
1018
|
-
|
|
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(
|
|
1045
|
-
|
|
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
|
|
1151
|
+
const stopTurn = () => {
|
|
1170
1152
|
interrupted = true;
|
|
1171
1153
|
abortRef?.abort();
|
|
1172
|
-
}
|
|
1173
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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;
|
|
@@ -1292,15 +1226,15 @@ async function executeStep(step, serverUrl, token, tunnel) {
|
|
|
1292
1226
|
try {
|
|
1293
1227
|
await runGit(["add", "-A"], cwd);
|
|
1294
1228
|
await pruneIgnoredFromIndex(cwd);
|
|
1295
|
-
await runGit(["commit", "--allow-empty", "-m", `checkpoint ${stepId}`], cwd);
|
|
1229
|
+
await runGit(["-c", "user.email=noreply@todos.dev", "-c", "user.name=Todos", "commit", "--allow-empty", "-m", `checkpoint ${stepId}`], cwd);
|
|
1296
1230
|
commitSha = await runGit(["rev-parse", "HEAD"], cwd);
|
|
1297
1231
|
} catch (err) {
|
|
1298
1232
|
console.warn(`[step] ${stepId} checkpoint commit failed:`, err.message);
|
|
1299
1233
|
}
|
|
1300
|
-
const {
|
|
1234
|
+
const { installationToken, defaultBranch } = step.workspace;
|
|
1301
1235
|
const branchName = `tds/conv-${conversationId}`;
|
|
1302
1236
|
try {
|
|
1303
|
-
await pushBranch(cwd,
|
|
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(
|
|
1318
|
-
stream.
|
|
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
|
|
1256
|
+
await done({ error: modelError });
|
|
1325
1257
|
} else {
|
|
1326
1258
|
await captureCheckpoint();
|
|
1327
|
-
await
|
|
1259
|
+
await done(donePayload());
|
|
1328
1260
|
}
|
|
1329
1261
|
} catch (err) {
|
|
1330
|
-
stream.
|
|
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
|
|
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
|
|
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
|
|
1282
|
+
const { stepId, conversationId } = step;
|
|
1352
1283
|
console.log(`[restore] ${stepId} start (conv ${conversationId})`);
|
|
1353
|
-
const
|
|
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 {
|
|
1359
|
-
await pushBranch(cwd,
|
|
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
|
|
1302
|
+
await done({ error: msg });
|
|
1372
1303
|
return;
|
|
1373
1304
|
}
|
|
1374
|
-
await
|
|
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
|
|
16071
|
-
|
|
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
|
-
|
|
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
|
-
"--
|
|
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
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
//
|
|
16792
|
-
//
|
|
16793
|
-
|
|
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
|
|
16787
|
+
const s = this.shellState(_TunnelServer.shellKey(evt.build, evt.machine ?? ""));
|
|
16840
16788
|
if (evt.event === "shell-opened") {
|
|
16841
|
-
|
|
16789
|
+
s.closed = false;
|
|
16842
16790
|
} else if (evt.event === "shell-output") {
|
|
16843
|
-
|
|
16844
|
-
|
|
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
|
-
|
|
16847
|
-
const
|
|
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.
|
|
16826
|
+
return this.shells.get(_TunnelServer.shellKey(build, machine))?.closed ?? false;
|
|
16877
16827
|
}
|
|
16878
16828
|
subscribeShell(build, machine, l) {
|
|
16879
|
-
const
|
|
16880
|
-
|
|
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
|
-
|
|
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
|
|
16898
|
-
|
|
16899
|
-
|
|
16900
|
-
|
|
16901
|
-
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
|
|
16905
|
-
|
|
16906
|
-
let
|
|
16907
|
-
if (
|
|
16908
|
-
|
|
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
|
|
16912
|
-
this.
|
|
16913
|
-
for (const l of
|
|
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
|
-
|
|
16939
|
-
|
|
16940
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
17900
|
-
|
|
17901
|
-
|
|
17902
|
-
|
|
17903
|
-
|
|
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.
|
|
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.
|
|
15
|
-
"@earendil-works/pi-coding-agent": "0.80.
|
|
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.
|
|
31
|
-
"@todos-dev/cli-darwin-x64": "0.1.
|
|
32
|
-
"@todos-dev/cli-linux-x64": "0.1.
|
|
33
|
-
"@todos-dev/cli-linux-arm64": "0.1.
|
|
34
|
-
"@todos-dev/cli-win32-x64": "0.1.
|
|
35
|
-
"@todos-dev/cli-win32-arm64": "0.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
|
}
|