@task0/cli 0.4.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +1923 -456
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -10,20 +10,23 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// ../../packages/shared/dist/node/yaml.js
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import yaml from "js-yaml";
|
|
16
16
|
function readYaml(filePath) {
|
|
17
|
-
if (!
|
|
17
|
+
if (!fs.existsSync(filePath))
|
|
18
18
|
return null;
|
|
19
|
-
const raw =
|
|
20
|
-
return
|
|
19
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
20
|
+
return yaml.load(raw);
|
|
21
|
+
}
|
|
22
|
+
function writeYaml(filePath, data) {
|
|
23
|
+
fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: 120 }), "utf-8");
|
|
21
24
|
}
|
|
22
25
|
function readContext(taskDir) {
|
|
23
|
-
const files =
|
|
26
|
+
const files = fs.readdirSync(taskDir).filter((f) => f.endsWith(".md"));
|
|
24
27
|
if (files.length === 0)
|
|
25
28
|
return void 0;
|
|
26
|
-
const content =
|
|
29
|
+
const content = fs.readFileSync(path.join(taskDir, files[0]), "utf-8");
|
|
27
30
|
const stripped = content.replace(/^---[\s\S]*?---\s*/, "");
|
|
28
31
|
return stripped.trim() || void 0;
|
|
29
32
|
}
|
|
@@ -141,29 +144,29 @@ var init_task = __esm({
|
|
|
141
144
|
});
|
|
142
145
|
|
|
143
146
|
// ../../packages/shared/dist/node/scanner.js
|
|
144
|
-
import
|
|
145
|
-
import
|
|
147
|
+
import fs2 from "fs";
|
|
148
|
+
import path2 from "path";
|
|
146
149
|
function scanProject(projectPath, sourceName) {
|
|
147
|
-
const absPath =
|
|
148
|
-
const resolvedSourceName = sourceName ??
|
|
150
|
+
const absPath = path2.resolve(projectPath);
|
|
151
|
+
const resolvedSourceName = sourceName ?? path2.basename(absPath);
|
|
149
152
|
const errors = [];
|
|
150
153
|
const tasks = [];
|
|
151
154
|
const repairs = [];
|
|
152
|
-
const projectYml =
|
|
155
|
+
const projectYml = path2.join(absPath, "task0.yml");
|
|
153
156
|
const projectConfig = readYaml(projectYml);
|
|
154
157
|
if (!projectConfig)
|
|
155
158
|
return { tasks, errors: [`${projectYml} not found`], repairs };
|
|
156
159
|
if (projectConfig.kind !== "project")
|
|
157
160
|
return { tasks, errors: [`Invalid: kind="${projectConfig.kind}"`], repairs };
|
|
158
|
-
const tasksDir =
|
|
159
|
-
if (!
|
|
161
|
+
const tasksDir = path2.join(absPath, projectConfig.tasks_dir);
|
|
162
|
+
if (!fs2.existsSync(tasksDir))
|
|
160
163
|
return { tasks, errors: [`tasks_dir not found: ${tasksDir}`], repairs };
|
|
161
|
-
const entries =
|
|
164
|
+
const entries = fs2.readdirSync(tasksDir, { withFileTypes: true });
|
|
162
165
|
for (const entry of entries) {
|
|
163
166
|
if (!entry.isDirectory())
|
|
164
167
|
continue;
|
|
165
|
-
const taskDir =
|
|
166
|
-
const taskYml =
|
|
168
|
+
const taskDir = path2.join(tasksDir, entry.name);
|
|
169
|
+
const taskYml = path2.join(taskDir, "task0.yml");
|
|
167
170
|
const raw = readYaml(taskYml);
|
|
168
171
|
if (!raw || raw.kind !== "task")
|
|
169
172
|
continue;
|
|
@@ -199,7 +202,7 @@ function scanProject(projectPath, sourceName) {
|
|
|
199
202
|
continue;
|
|
200
203
|
}
|
|
201
204
|
const context = readContext(taskDir);
|
|
202
|
-
const stat =
|
|
205
|
+
const stat = fs2.statSync(taskYml);
|
|
203
206
|
const tags = raw.tags || [];
|
|
204
207
|
const summary = raw.summary || void 0;
|
|
205
208
|
const displayTitle = summary?.title || title;
|
|
@@ -242,18 +245,18 @@ var init_scanner = __esm({
|
|
|
242
245
|
});
|
|
243
246
|
|
|
244
247
|
// ../../packages/shared/dist/node/open-questions.js
|
|
245
|
-
import
|
|
246
|
-
import
|
|
248
|
+
import fs3 from "fs";
|
|
249
|
+
import path3 from "path";
|
|
247
250
|
function readOpenQuestions(taskDir, issueFiles) {
|
|
248
251
|
const result = [];
|
|
249
252
|
for (const file of issueFiles) {
|
|
250
253
|
const m = file.match(/^ISSUE-(\d+)\.md$/);
|
|
251
254
|
if (!m)
|
|
252
255
|
continue;
|
|
253
|
-
const fullPath =
|
|
254
|
-
if (!
|
|
256
|
+
const fullPath = path3.join(taskDir, file);
|
|
257
|
+
if (!fs3.existsSync(fullPath))
|
|
255
258
|
continue;
|
|
256
|
-
const md =
|
|
259
|
+
const md = fs3.readFileSync(fullPath, "utf-8");
|
|
257
260
|
const match = md.match(OPEN_QUESTIONS_SECTION_RE);
|
|
258
261
|
if (!match)
|
|
259
262
|
continue;
|
|
@@ -276,34 +279,34 @@ var init_open_questions = __esm({
|
|
|
276
279
|
});
|
|
277
280
|
|
|
278
281
|
// ../../packages/shared/dist/node/task-state.js
|
|
279
|
-
import
|
|
282
|
+
import fs4 from "fs";
|
|
280
283
|
import os from "os";
|
|
281
|
-
import
|
|
282
|
-
import
|
|
284
|
+
import path4 from "path";
|
|
285
|
+
import yaml2 from "js-yaml";
|
|
283
286
|
function findProjectRoot(start = process.cwd()) {
|
|
284
|
-
let dir =
|
|
287
|
+
let dir = path4.resolve(start);
|
|
285
288
|
while (true) {
|
|
286
|
-
const ymlPath =
|
|
287
|
-
if (
|
|
289
|
+
const ymlPath = path4.join(dir, "task0.yml");
|
|
290
|
+
if (fs4.existsSync(ymlPath)) {
|
|
288
291
|
try {
|
|
289
|
-
const raw =
|
|
292
|
+
const raw = yaml2.load(fs4.readFileSync(ymlPath, "utf-8"));
|
|
290
293
|
if (raw?.kind === "project")
|
|
291
294
|
return dir;
|
|
292
295
|
} catch {
|
|
293
296
|
}
|
|
294
297
|
}
|
|
295
|
-
const parent =
|
|
298
|
+
const parent = path4.dirname(dir);
|
|
296
299
|
if (parent === dir)
|
|
297
300
|
return null;
|
|
298
301
|
dir = parent;
|
|
299
302
|
}
|
|
300
303
|
}
|
|
301
304
|
function readProjectConfig(projectRoot) {
|
|
302
|
-
return
|
|
305
|
+
return yaml2.load(fs4.readFileSync(path4.join(projectRoot, "task0.yml"), "utf-8"));
|
|
303
306
|
}
|
|
304
307
|
function resolveTasksDir(projectRoot, projectConfig) {
|
|
305
308
|
const cfg = projectConfig ?? readProjectConfig(projectRoot);
|
|
306
|
-
return
|
|
309
|
+
return path4.isAbsolute(cfg.tasks_dir) ? cfg.tasks_dir : path4.join(projectRoot, cfg.tasks_dir);
|
|
307
310
|
}
|
|
308
311
|
function resolveTaskByObjectId(objectId, projectRoot) {
|
|
309
312
|
const root = projectRoot ?? findProjectRoot();
|
|
@@ -314,16 +317,16 @@ function resolveTaskByObjectId(objectId, projectRoot) {
|
|
|
314
317
|
throw new Error(`Expected a task object_id like 'tsk_XXXXX', got '${objectId}'. Directory names are no longer accepted; run 'task0 task list' to find the object_id.`);
|
|
315
318
|
}
|
|
316
319
|
const tasksDir = resolveTasksDir(root);
|
|
317
|
-
const entries =
|
|
320
|
+
const entries = fs4.readdirSync(tasksDir, { withFileTypes: true });
|
|
318
321
|
for (const entry of entries) {
|
|
319
322
|
if (!entry.isDirectory())
|
|
320
323
|
continue;
|
|
321
|
-
const taskDir =
|
|
322
|
-
const taskYml =
|
|
323
|
-
if (!
|
|
324
|
+
const taskDir = path4.join(tasksDir, entry.name);
|
|
325
|
+
const taskYml = path4.join(taskDir, "task0.yml");
|
|
326
|
+
if (!fs4.existsSync(taskYml))
|
|
324
327
|
continue;
|
|
325
328
|
try {
|
|
326
|
-
const raw =
|
|
329
|
+
const raw = yaml2.load(fs4.readFileSync(taskYml, "utf-8"));
|
|
327
330
|
if (raw && raw.object_id === objectId) {
|
|
328
331
|
return { projectRoot: root, tasksDir, taskDir, taskYml };
|
|
329
332
|
}
|
|
@@ -333,15 +336,15 @@ function resolveTaskByObjectId(objectId, projectRoot) {
|
|
|
333
336
|
throw new Error(`No task with object_id '${objectId}' found under ${tasksDir}. If tasks pre-date object_id, run 'task0 task migrate' to seed them.`);
|
|
334
337
|
}
|
|
335
338
|
function readTaskYaml(taskYml) {
|
|
336
|
-
if (!
|
|
339
|
+
if (!fs4.existsSync(taskYml))
|
|
337
340
|
throw new Error(`task0.yml not found: ${taskYml}`);
|
|
338
|
-
return
|
|
341
|
+
return yaml2.load(fs4.readFileSync(taskYml, "utf-8")) || {};
|
|
339
342
|
}
|
|
340
343
|
function writeTaskYaml(taskYml, data) {
|
|
341
|
-
|
|
344
|
+
fs4.writeFileSync(taskYml, yaml2.dump(data, { lineWidth: 120 }), "utf-8");
|
|
342
345
|
}
|
|
343
346
|
function taskYamlLockPath(taskDir) {
|
|
344
|
-
return
|
|
347
|
+
return path4.join(taskDir, TASK_YAML_LOCKFILE);
|
|
345
348
|
}
|
|
346
349
|
function isProcessAlive(pid) {
|
|
347
350
|
try {
|
|
@@ -353,17 +356,17 @@ function isProcessAlive(pid) {
|
|
|
353
356
|
}
|
|
354
357
|
function readTaskYamlLockInfo(file) {
|
|
355
358
|
try {
|
|
356
|
-
return JSON.parse(
|
|
359
|
+
return JSON.parse(fs4.readFileSync(file, "utf-8"));
|
|
357
360
|
} catch {
|
|
358
361
|
return null;
|
|
359
362
|
}
|
|
360
363
|
}
|
|
361
364
|
function writeTaskYamlLockInfo(file, info) {
|
|
362
|
-
const fd =
|
|
365
|
+
const fd = fs4.openSync(file, "wx");
|
|
363
366
|
try {
|
|
364
|
-
|
|
367
|
+
fs4.writeFileSync(fd, JSON.stringify(info, null, 2), "utf-8");
|
|
365
368
|
} finally {
|
|
366
|
-
|
|
369
|
+
fs4.closeSync(fd);
|
|
367
370
|
}
|
|
368
371
|
}
|
|
369
372
|
function sleep(ms) {
|
|
@@ -389,7 +392,7 @@ async function acquireTaskYamlLock(taskDir) {
|
|
|
389
392
|
const existing = readTaskYamlLockInfo(file);
|
|
390
393
|
if (existing && existing.hostname === info.hostname && !isProcessAlive(existing.pid)) {
|
|
391
394
|
try {
|
|
392
|
-
|
|
395
|
+
fs4.unlinkSync(file);
|
|
393
396
|
continue;
|
|
394
397
|
} catch (err) {
|
|
395
398
|
if (err.code !== "ENOENT") {
|
|
@@ -411,7 +414,7 @@ function releaseTaskYamlLock(taskDir, info) {
|
|
|
411
414
|
return;
|
|
412
415
|
}
|
|
413
416
|
try {
|
|
414
|
-
|
|
417
|
+
fs4.unlinkSync(file);
|
|
415
418
|
} catch (err) {
|
|
416
419
|
if (err.code !== "ENOENT") {
|
|
417
420
|
throw err;
|
|
@@ -419,7 +422,7 @@ function releaseTaskYamlLock(taskDir, info) {
|
|
|
419
422
|
}
|
|
420
423
|
}
|
|
421
424
|
async function withTaskYamlLock(taskDir, fn) {
|
|
422
|
-
const key =
|
|
425
|
+
const key = path4.resolve(taskDir);
|
|
423
426
|
const prev = yamlLocks.get(key) ?? Promise.resolve();
|
|
424
427
|
const next = prev.then(async () => {
|
|
425
428
|
const lock = await acquireTaskYamlLock(taskDir);
|
|
@@ -450,7 +453,7 @@ function readTaskWorkflow(taskYml) {
|
|
|
450
453
|
return raw.workflow || {};
|
|
451
454
|
}
|
|
452
455
|
async function updateTaskWorkflow(taskYml, patch) {
|
|
453
|
-
const taskDir =
|
|
456
|
+
const taskDir = path4.dirname(taskYml);
|
|
454
457
|
return withTaskYamlLock(taskDir, () => {
|
|
455
458
|
const raw = readTaskYaml(taskYml);
|
|
456
459
|
const current = raw.workflow || {};
|
|
@@ -580,19 +583,19 @@ var init_redact = __esm({
|
|
|
580
583
|
});
|
|
581
584
|
|
|
582
585
|
// ../../packages/shared/dist/node/error-reports.js
|
|
583
|
-
import
|
|
586
|
+
import fs5 from "fs";
|
|
584
587
|
import os2 from "os";
|
|
585
|
-
import
|
|
588
|
+
import path5 from "path";
|
|
586
589
|
import { spawnSync } from "child_process";
|
|
587
590
|
import crypto from "crypto";
|
|
588
591
|
function task0Home() {
|
|
589
592
|
const override = process.env.TASK0_HOME;
|
|
590
593
|
if (override && override.length > 0)
|
|
591
594
|
return override;
|
|
592
|
-
return
|
|
595
|
+
return path5.join(os2.homedir(), ".task0");
|
|
593
596
|
}
|
|
594
597
|
function errorsRoot() {
|
|
595
|
-
return
|
|
598
|
+
return path5.join(task0Home(), "errors");
|
|
596
599
|
}
|
|
597
600
|
function createErrorReportId() {
|
|
598
601
|
return `err_${crypto.randomBytes(4).toString("hex")}`;
|
|
@@ -693,13 +696,13 @@ function buildErrorReport(input) {
|
|
|
693
696
|
function writeErrorReportSync(report, rootOverride) {
|
|
694
697
|
const root = rootOverride ?? errorsRoot();
|
|
695
698
|
const dirName = errorReportDirName(new Date(report.captured_at), report.id);
|
|
696
|
-
const dir =
|
|
697
|
-
|
|
698
|
-
const finalPath =
|
|
699
|
-
const tmpPath =
|
|
699
|
+
const dir = path5.join(root, dirName);
|
|
700
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
701
|
+
const finalPath = path5.join(dir, REPORT_FILENAME);
|
|
702
|
+
const tmpPath = path5.join(dir, TMP_FILENAME);
|
|
700
703
|
const json = JSON.stringify(report, null, 2);
|
|
701
|
-
|
|
702
|
-
|
|
704
|
+
fs5.writeFileSync(tmpPath, json, "utf-8");
|
|
705
|
+
fs5.renameSync(tmpPath, finalPath);
|
|
703
706
|
return { dir, path: finalPath };
|
|
704
707
|
}
|
|
705
708
|
function parseReportDir(name) {
|
|
@@ -716,7 +719,7 @@ function listErrorReports(rootOverride) {
|
|
|
716
719
|
};
|
|
717
720
|
let entries;
|
|
718
721
|
try {
|
|
719
|
-
entries =
|
|
722
|
+
entries = fs5.readdirSync(root, { withFileTypes: true });
|
|
720
723
|
} catch (err) {
|
|
721
724
|
if (err.code === "ENOENT")
|
|
722
725
|
return result;
|
|
@@ -728,11 +731,11 @@ function listErrorReports(rootOverride) {
|
|
|
728
731
|
const parsed = parseReportDir(entry.name);
|
|
729
732
|
if (!parsed)
|
|
730
733
|
continue;
|
|
731
|
-
const dir =
|
|
732
|
-
const file =
|
|
734
|
+
const dir = path5.join(root, entry.name);
|
|
735
|
+
const file = path5.join(dir, REPORT_FILENAME);
|
|
733
736
|
let raw;
|
|
734
737
|
try {
|
|
735
|
-
raw =
|
|
738
|
+
raw = fs5.readFileSync(file, "utf-8");
|
|
736
739
|
} catch {
|
|
737
740
|
result.skipped.unreadable += 1;
|
|
738
741
|
continue;
|
|
@@ -750,7 +753,7 @@ function listErrorReports(rootOverride) {
|
|
|
750
753
|
}
|
|
751
754
|
let size = 0;
|
|
752
755
|
try {
|
|
753
|
-
size =
|
|
756
|
+
size = fs5.statSync(file).size;
|
|
754
757
|
} catch {
|
|
755
758
|
}
|
|
756
759
|
result.reports.push({
|
|
@@ -785,7 +788,7 @@ function resolveErrorReport(query, rootOverride) {
|
|
|
785
788
|
return { kind: "miss", query };
|
|
786
789
|
}
|
|
787
790
|
function readErrorReport(summary) {
|
|
788
|
-
const raw =
|
|
791
|
+
const raw = fs5.readFileSync(summary.path, "utf-8");
|
|
789
792
|
return JSON.parse(raw);
|
|
790
793
|
}
|
|
791
794
|
function pruneErrorReports(opts, rootOverride) {
|
|
@@ -823,7 +826,7 @@ function pruneErrorReports(opts, rootOverride) {
|
|
|
823
826
|
}
|
|
824
827
|
function removeReportDir(dir) {
|
|
825
828
|
try {
|
|
826
|
-
|
|
829
|
+
fs5.rmSync(dir, { recursive: true, force: true });
|
|
827
830
|
return true;
|
|
828
831
|
} catch {
|
|
829
832
|
return false;
|
|
@@ -841,9 +844,9 @@ var init_error_reports = __esm({
|
|
|
841
844
|
});
|
|
842
845
|
|
|
843
846
|
// ../../packages/shared/dist/node/file-lock.js
|
|
844
|
-
import
|
|
847
|
+
import fs6 from "fs";
|
|
845
848
|
import os3 from "os";
|
|
846
|
-
import
|
|
849
|
+
import path6 from "path";
|
|
847
850
|
var init_file_lock = __esm({
|
|
848
851
|
"../../packages/shared/dist/node/file-lock.js"() {
|
|
849
852
|
"use strict";
|
|
@@ -858,6 +861,202 @@ var init_tmux = __esm({
|
|
|
858
861
|
}
|
|
859
862
|
});
|
|
860
863
|
|
|
864
|
+
// ../../packages/shared/dist/node/agent-skills.js
|
|
865
|
+
import fs7 from "fs";
|
|
866
|
+
import path7 from "path";
|
|
867
|
+
import yaml3 from "js-yaml";
|
|
868
|
+
function isRecord(value) {
|
|
869
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
870
|
+
}
|
|
871
|
+
function extractFrontmatter(raw) {
|
|
872
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
873
|
+
if (!match)
|
|
874
|
+
return null;
|
|
875
|
+
try {
|
|
876
|
+
const parsed = yaml3.load(match[1]);
|
|
877
|
+
return isRecord(parsed) ? parsed : null;
|
|
878
|
+
} catch {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function readTextIfExists(filePath) {
|
|
883
|
+
try {
|
|
884
|
+
if (!fs7.existsSync(filePath))
|
|
885
|
+
return null;
|
|
886
|
+
return fs7.readFileSync(filePath, "utf-8");
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function getSymlinkInfo(...candidatePaths) {
|
|
892
|
+
for (const candidatePath of candidatePaths) {
|
|
893
|
+
try {
|
|
894
|
+
const stat = fs7.lstatSync(candidatePath);
|
|
895
|
+
if (!stat.isSymbolicLink())
|
|
896
|
+
continue;
|
|
897
|
+
const target = fs7.readlinkSync(candidatePath);
|
|
898
|
+
return {
|
|
899
|
+
isSymlink: true,
|
|
900
|
+
symlinkTarget: path7.isAbsolute(target) ? target : path7.resolve(path7.dirname(candidatePath), target)
|
|
901
|
+
};
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return { isSymlink: false };
|
|
906
|
+
}
|
|
907
|
+
function normalizeString(value) {
|
|
908
|
+
return typeof value === "string" ? value.trim() : "";
|
|
909
|
+
}
|
|
910
|
+
function normalizeGlobs(value) {
|
|
911
|
+
if (typeof value === "string") {
|
|
912
|
+
const glob = value.trim();
|
|
913
|
+
return glob ? [glob] : void 0;
|
|
914
|
+
}
|
|
915
|
+
if (Array.isArray(value)) {
|
|
916
|
+
const globs = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
|
|
917
|
+
return globs.length > 0 ? globs : void 0;
|
|
918
|
+
}
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
function normalizeBoolean(value) {
|
|
922
|
+
if (typeof value === "boolean")
|
|
923
|
+
return value;
|
|
924
|
+
if (value === "true")
|
|
925
|
+
return true;
|
|
926
|
+
if (value === "false")
|
|
927
|
+
return false;
|
|
928
|
+
return void 0;
|
|
929
|
+
}
|
|
930
|
+
function sortSkills(skills) {
|
|
931
|
+
return skills.sort((a, b) => AGENT_ORDER[a.agent] - AGENT_ORDER[b.agent] || SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope] || KIND_ORDER[a.kind] - KIND_ORDER[b.kind] || a.name.localeCompare(b.name) || a.filePath.localeCompare(b.filePath));
|
|
932
|
+
}
|
|
933
|
+
function pushInstructionIfExists(skills, agent2, scope, filePath, name = path7.basename(filePath), description2 = "", kind = "instruction") {
|
|
934
|
+
const raw = readTextIfExists(filePath);
|
|
935
|
+
if (raw === null)
|
|
936
|
+
return;
|
|
937
|
+
skills.push({
|
|
938
|
+
agent: agent2,
|
|
939
|
+
scope,
|
|
940
|
+
kind,
|
|
941
|
+
name,
|
|
942
|
+
description: description2,
|
|
943
|
+
filePath,
|
|
944
|
+
...getSymlinkInfo(filePath)
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function scanClaudeSkillDir(skills, skillsDir, scope) {
|
|
948
|
+
let entries = [];
|
|
949
|
+
try {
|
|
950
|
+
if (!fs7.existsSync(skillsDir))
|
|
951
|
+
return;
|
|
952
|
+
entries = fs7.readdirSync(skillsDir, { withFileTypes: true });
|
|
953
|
+
} catch {
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
for (const entry of entries) {
|
|
957
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink())
|
|
958
|
+
continue;
|
|
959
|
+
const skillDirPath = path7.join(skillsDir, entry.name);
|
|
960
|
+
const skillFilePath = path7.join(skillDirPath, "SKILL.md");
|
|
961
|
+
const raw = readTextIfExists(skillFilePath);
|
|
962
|
+
if (raw === null)
|
|
963
|
+
continue;
|
|
964
|
+
const frontmatter = extractFrontmatter(raw);
|
|
965
|
+
skills.push({
|
|
966
|
+
agent: "claude_code",
|
|
967
|
+
scope,
|
|
968
|
+
kind: "skill",
|
|
969
|
+
name: normalizeString(frontmatter?.name) || entry.name,
|
|
970
|
+
description: normalizeString(frontmatter?.description),
|
|
971
|
+
filePath: skillFilePath,
|
|
972
|
+
...getSymlinkInfo(skillDirPath, skillFilePath)
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function scanCursorRuleFile(skills, filePath, scope) {
|
|
977
|
+
const raw = readTextIfExists(filePath);
|
|
978
|
+
if (raw === null)
|
|
979
|
+
return;
|
|
980
|
+
const frontmatter = extractFrontmatter(raw);
|
|
981
|
+
const baseName = path7.basename(filePath, ".mdc");
|
|
982
|
+
skills.push({
|
|
983
|
+
agent: "cursor",
|
|
984
|
+
scope,
|
|
985
|
+
kind: "rule",
|
|
986
|
+
name: normalizeString(frontmatter?.name) || baseName,
|
|
987
|
+
description: normalizeString(frontmatter?.description),
|
|
988
|
+
filePath,
|
|
989
|
+
globs: normalizeGlobs(frontmatter?.globs),
|
|
990
|
+
alwaysApply: normalizeBoolean(frontmatter?.alwaysApply),
|
|
991
|
+
...getSymlinkInfo(filePath)
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
function scanCursorRulesDir(skills, rulesDir, scope) {
|
|
995
|
+
let entries = [];
|
|
996
|
+
try {
|
|
997
|
+
if (!fs7.existsSync(rulesDir))
|
|
998
|
+
return;
|
|
999
|
+
entries = fs7.readdirSync(rulesDir, { withFileTypes: true });
|
|
1000
|
+
} catch {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
for (const entry of entries) {
|
|
1004
|
+
if (!entry.name.endsWith(".mdc"))
|
|
1005
|
+
continue;
|
|
1006
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
1007
|
+
continue;
|
|
1008
|
+
scanCursorRuleFile(skills, path7.join(rulesDir, entry.name), scope);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function getProjectAgentSkills(projectPath) {
|
|
1012
|
+
const absProjectPath = path7.resolve(projectPath);
|
|
1013
|
+
let projectStat;
|
|
1014
|
+
try {
|
|
1015
|
+
projectStat = fs7.statSync(absProjectPath);
|
|
1016
|
+
} catch {
|
|
1017
|
+
return [];
|
|
1018
|
+
}
|
|
1019
|
+
if (!projectStat.isDirectory())
|
|
1020
|
+
return [];
|
|
1021
|
+
const skills = [];
|
|
1022
|
+
scanClaudeSkillDir(skills, path7.join(absProjectPath, ".claude", "skills"), "project");
|
|
1023
|
+
pushInstructionIfExists(skills, "claude_code", "project", path7.join(absProjectPath, "CLAUDE.md"));
|
|
1024
|
+
pushInstructionIfExists(skills, "claude_code", "project", path7.join(absProjectPath, "AGENTS.md"));
|
|
1025
|
+
pushInstructionIfExists(skills, "codex", "project", path7.join(absProjectPath, "AGENTS.md"));
|
|
1026
|
+
scanCursorRulesDir(skills, path7.join(absProjectPath, ".cursor", "rules"), "project");
|
|
1027
|
+
pushInstructionIfExists(skills, "cursor", "project", path7.join(absProjectPath, ".cursorrules"), "Legacy Cursor Rules", "", "rule");
|
|
1028
|
+
return sortSkills(skills);
|
|
1029
|
+
}
|
|
1030
|
+
function getGlobalAgentSkills() {
|
|
1031
|
+
const homeDir = process.env.HOME;
|
|
1032
|
+
if (!homeDir)
|
|
1033
|
+
return [];
|
|
1034
|
+
const skills = [];
|
|
1035
|
+
scanClaudeSkillDir(skills, path7.join(homeDir, ".claude", "skills"), "global");
|
|
1036
|
+
scanCursorRulesDir(skills, path7.join(homeDir, ".cursor", "rules"), "global");
|
|
1037
|
+
return sortSkills(skills);
|
|
1038
|
+
}
|
|
1039
|
+
var AGENT_ORDER, SCOPE_ORDER, KIND_ORDER;
|
|
1040
|
+
var init_agent_skills = __esm({
|
|
1041
|
+
"../../packages/shared/dist/node/agent-skills.js"() {
|
|
1042
|
+
"use strict";
|
|
1043
|
+
AGENT_ORDER = {
|
|
1044
|
+
claude_code: 0,
|
|
1045
|
+
codex: 1,
|
|
1046
|
+
cursor: 2
|
|
1047
|
+
};
|
|
1048
|
+
SCOPE_ORDER = {
|
|
1049
|
+
project: 0,
|
|
1050
|
+
global: 1
|
|
1051
|
+
};
|
|
1052
|
+
KIND_ORDER = {
|
|
1053
|
+
skill: 0,
|
|
1054
|
+
rule: 1,
|
|
1055
|
+
instruction: 2
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
861
1060
|
// ../../packages/shared/dist/node/index.js
|
|
862
1061
|
var init_node = __esm({
|
|
863
1062
|
"../../packages/shared/dist/node/index.js"() {
|
|
@@ -870,6 +1069,7 @@ var init_node = __esm({
|
|
|
870
1069
|
init_redact();
|
|
871
1070
|
init_file_lock();
|
|
872
1071
|
init_tmux();
|
|
1072
|
+
init_agent_skills();
|
|
873
1073
|
init_error_report();
|
|
874
1074
|
}
|
|
875
1075
|
});
|
|
@@ -889,7 +1089,7 @@ __export(task_state_exports, {
|
|
|
889
1089
|
withTaskYamlLock: () => withTaskYamlLock,
|
|
890
1090
|
writeTaskYaml: () => writeTaskYaml
|
|
891
1091
|
});
|
|
892
|
-
import
|
|
1092
|
+
import fs12 from "fs";
|
|
893
1093
|
function readWorkflow(taskYml) {
|
|
894
1094
|
return readTaskWorkflow(taskYml);
|
|
895
1095
|
}
|
|
@@ -898,14 +1098,14 @@ async function updateWorkflow(taskYml, patch) {
|
|
|
898
1098
|
}
|
|
899
1099
|
function nextArtifactIndex(taskDir, prefix, ext = "md") {
|
|
900
1100
|
const pattern = new RegExp(`^${prefix}-(\\d+).*\\.${ext}$`);
|
|
901
|
-
const entries =
|
|
1101
|
+
const entries = fs12.readdirSync(taskDir);
|
|
902
1102
|
const indices = entries.map((name) => name.match(pattern)?.[1]).filter((v) => v != null).map(Number);
|
|
903
1103
|
const next = indices.length > 0 ? Math.max(...indices) + 1 : 1;
|
|
904
1104
|
return String(next).padStart(2, "0");
|
|
905
1105
|
}
|
|
906
1106
|
function latestArtifact(taskDir, pattern) {
|
|
907
|
-
if (!
|
|
908
|
-
const matches =
|
|
1107
|
+
if (!fs12.existsSync(taskDir)) return null;
|
|
1108
|
+
const matches = fs12.readdirSync(taskDir).filter((name) => pattern.test(name));
|
|
909
1109
|
if (matches.length === 0) return null;
|
|
910
1110
|
matches.sort();
|
|
911
1111
|
return matches[matches.length - 1] || null;
|
|
@@ -918,44 +1118,115 @@ var init_task_state2 = __esm({
|
|
|
918
1118
|
});
|
|
919
1119
|
|
|
920
1120
|
// src/main.ts
|
|
921
|
-
import {
|
|
922
|
-
import
|
|
923
|
-
import path25 from "path";
|
|
924
|
-
import { Command as Command23 } from "commander";
|
|
925
|
-
|
|
926
|
-
// src/commands/source.ts
|
|
927
|
-
import { Command } from "commander";
|
|
928
|
-
import path8 from "path";
|
|
929
|
-
import chalk from "chalk";
|
|
1121
|
+
import { Command as Command24 } from "commander";
|
|
1122
|
+
import chalk24 from "chalk";
|
|
930
1123
|
|
|
931
1124
|
// src/core/config.ts
|
|
932
|
-
|
|
933
|
-
import
|
|
934
|
-
import
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
)
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1125
|
+
init_node();
|
|
1126
|
+
import fs8 from "fs";
|
|
1127
|
+
import os4 from "os";
|
|
1128
|
+
import path8 from "path";
|
|
1129
|
+
import yaml4 from "js-yaml";
|
|
1130
|
+
function registryDir() {
|
|
1131
|
+
return path8.join(
|
|
1132
|
+
process.env.HOME || process.env.USERPROFILE || os4.homedir(),
|
|
1133
|
+
".config",
|
|
1134
|
+
"task0"
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
function registryFile() {
|
|
1138
|
+
return path8.join(registryDir(), "config.yml");
|
|
1139
|
+
}
|
|
1140
|
+
function homeStateFile() {
|
|
1141
|
+
return path8.join(task0Home(), "config.yml");
|
|
1142
|
+
}
|
|
1143
|
+
function configFilePath() {
|
|
1144
|
+
return registryFile();
|
|
1145
|
+
}
|
|
1146
|
+
function readYamlFile(file) {
|
|
1147
|
+
if (!fs8.existsSync(file)) return null;
|
|
1148
|
+
try {
|
|
1149
|
+
const raw = fs8.readFileSync(file, "utf-8");
|
|
1150
|
+
const parsed = yaml4.load(raw);
|
|
1151
|
+
return parsed ?? null;
|
|
1152
|
+
} catch {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
function writeYamlFile(file, data) {
|
|
1157
|
+
fs8.mkdirSync(path8.dirname(file), { recursive: true });
|
|
1158
|
+
fs8.writeFileSync(file, yaml4.dump(data, { lineWidth: 120 }), "utf-8");
|
|
1159
|
+
}
|
|
1160
|
+
function loadRegistry() {
|
|
1161
|
+
return readYamlFile(registryFile()) ?? {};
|
|
1162
|
+
}
|
|
1163
|
+
function saveRegistry(data) {
|
|
1164
|
+
writeYamlFile(registryFile(), data);
|
|
943
1165
|
}
|
|
944
|
-
function
|
|
945
|
-
return
|
|
1166
|
+
function loadHomeState() {
|
|
1167
|
+
return readYamlFile(homeStateFile()) ?? {};
|
|
1168
|
+
}
|
|
1169
|
+
function saveHomeState(data) {
|
|
1170
|
+
writeYamlFile(homeStateFile(), data);
|
|
946
1171
|
}
|
|
947
1172
|
function loadConfig() {
|
|
948
|
-
|
|
949
|
-
const
|
|
950
|
-
const
|
|
1173
|
+
const registry = loadRegistry();
|
|
1174
|
+
const home = loadHomeState();
|
|
1175
|
+
const profiles = registry.profiles && typeof registry.profiles === "object" && !Array.isArray(registry.profiles) ? registry.profiles : void 0;
|
|
1176
|
+
const current = typeof registry.current_profile === "string" && registry.current_profile.length > 0 ? registry.current_profile : void 0;
|
|
1177
|
+
const sources = Array.isArray(home.sources) ? home.sources : Array.isArray(registry.sources) ? registry.sources : [];
|
|
1178
|
+
const agentModels = home.agentModels !== void 0 ? home.agentModels : registry.agentModels;
|
|
951
1179
|
return {
|
|
952
|
-
|
|
953
|
-
|
|
1180
|
+
sources,
|
|
1181
|
+
...agentModels !== void 0 ? { agentModels } : {},
|
|
1182
|
+
...profiles ? { profiles } : {},
|
|
1183
|
+
...current ? { current_profile: current } : {}
|
|
954
1184
|
};
|
|
955
1185
|
}
|
|
956
1186
|
function saveConfig(config) {
|
|
957
|
-
|
|
958
|
-
|
|
1187
|
+
const { sources, agentModels, ...registryFields } = config;
|
|
1188
|
+
saveRegistry(registryFields);
|
|
1189
|
+
saveHomeState({
|
|
1190
|
+
...sources !== void 0 ? { sources } : {},
|
|
1191
|
+
...agentModels !== void 0 ? { agentModels } : {}
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
function listProfiles() {
|
|
1195
|
+
return loadConfig().profiles ?? {};
|
|
1196
|
+
}
|
|
1197
|
+
function getProfile(name) {
|
|
1198
|
+
return listProfiles()[name];
|
|
1199
|
+
}
|
|
1200
|
+
function getCurrentProfileName() {
|
|
1201
|
+
return loadConfig().current_profile;
|
|
1202
|
+
}
|
|
1203
|
+
function addProfile(name, entry) {
|
|
1204
|
+
const config = loadConfig();
|
|
1205
|
+
const profiles = { ...config.profiles ?? {} };
|
|
1206
|
+
profiles[name] = entry;
|
|
1207
|
+
config.profiles = profiles;
|
|
1208
|
+
saveConfig(config);
|
|
1209
|
+
}
|
|
1210
|
+
function removeProfile(name) {
|
|
1211
|
+
const config = loadConfig();
|
|
1212
|
+
if (!config.profiles || !(name in config.profiles)) return false;
|
|
1213
|
+
const profiles = { ...config.profiles };
|
|
1214
|
+
delete profiles[name];
|
|
1215
|
+
config.profiles = profiles;
|
|
1216
|
+
if (config.current_profile === name) {
|
|
1217
|
+
delete config.current_profile;
|
|
1218
|
+
}
|
|
1219
|
+
saveConfig(config);
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
function setCurrentProfile(name) {
|
|
1223
|
+
const config = loadConfig();
|
|
1224
|
+
if (name === null) {
|
|
1225
|
+
delete config.current_profile;
|
|
1226
|
+
} else {
|
|
1227
|
+
config.current_profile = name;
|
|
1228
|
+
}
|
|
1229
|
+
saveConfig(config);
|
|
959
1230
|
}
|
|
960
1231
|
function addSource(entry) {
|
|
961
1232
|
const config = loadConfig();
|
|
@@ -975,96 +1246,370 @@ function removeSource(name) {
|
|
|
975
1246
|
saveConfig(config);
|
|
976
1247
|
return true;
|
|
977
1248
|
}
|
|
978
|
-
|
|
979
|
-
|
|
1249
|
+
|
|
1250
|
+
// src/core/profile.ts
|
|
1251
|
+
var ProfileNotFoundError = class extends Error {
|
|
1252
|
+
constructor(name, available) {
|
|
1253
|
+
const list = available.length > 0 ? available.join(", ") : "(none)";
|
|
1254
|
+
super(`Profile "${name}" not found. Available: ${list}. Run "task0 profile list" to inspect.`);
|
|
1255
|
+
this.name = "ProfileNotFoundError";
|
|
1256
|
+
}
|
|
1257
|
+
};
|
|
1258
|
+
function parseProfileFlag(argv) {
|
|
1259
|
+
for (let i = 2; i < argv.length; i++) {
|
|
1260
|
+
const arg = argv[i];
|
|
1261
|
+
if (arg === "--") return null;
|
|
1262
|
+
if (arg === "--profile") {
|
|
1263
|
+
const next = argv[i + 1];
|
|
1264
|
+
if (typeof next === "string" && !next.startsWith("-")) return next;
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
if (typeof arg === "string" && arg.startsWith("--profile=")) {
|
|
1268
|
+
const value = arg.slice("--profile=".length);
|
|
1269
|
+
return value.length > 0 ? value : null;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return null;
|
|
1273
|
+
}
|
|
1274
|
+
function isProfileSubcommandInvocation(argv) {
|
|
1275
|
+
for (let i = 2; i < argv.length; i++) {
|
|
1276
|
+
const arg = argv[i];
|
|
1277
|
+
if (arg === "--") break;
|
|
1278
|
+
if (typeof arg !== "string") continue;
|
|
1279
|
+
if (arg.startsWith("-")) {
|
|
1280
|
+
if (arg === "--profile" || arg === "-C") i++;
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
return arg === "profile";
|
|
1284
|
+
}
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
function resolveActiveProfile(flagValue) {
|
|
1288
|
+
const requested = flagValue ?? getCurrentProfileName() ?? null;
|
|
1289
|
+
if (!requested) return null;
|
|
1290
|
+
const entry = getProfile(requested);
|
|
1291
|
+
if (!entry) {
|
|
1292
|
+
throw new ProfileNotFoundError(requested, Object.keys(listProfiles()));
|
|
1293
|
+
}
|
|
1294
|
+
return { name: requested, entry };
|
|
1295
|
+
}
|
|
1296
|
+
var activeProfileCache = null;
|
|
1297
|
+
function activateProfile(argv) {
|
|
1298
|
+
const flagValue = parseProfileFlag(argv);
|
|
1299
|
+
let active2;
|
|
1300
|
+
try {
|
|
1301
|
+
active2 = resolveActiveProfile(flagValue);
|
|
1302
|
+
} catch (error2) {
|
|
1303
|
+
if (isProfileSubcommandInvocation(argv) && error2 instanceof ProfileNotFoundError && flagValue === null) {
|
|
1304
|
+
active2 = null;
|
|
1305
|
+
} else {
|
|
1306
|
+
throw error2;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (active2) {
|
|
1310
|
+
activeProfileCache = active2;
|
|
1311
|
+
if (active2.entry.task0_home && active2.entry.task0_home.length > 0) {
|
|
1312
|
+
process.env.TASK0_HOME = active2.entry.task0_home;
|
|
1313
|
+
}
|
|
1314
|
+
if (active2.entry.api_url && !process.env.TASK0_API_URL) {
|
|
1315
|
+
process.env.TASK0_API_URL = active2.entry.api_url;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
980
1318
|
}
|
|
981
1319
|
|
|
982
1320
|
// src/commands/source.ts
|
|
983
|
-
|
|
1321
|
+
import { Command } from "commander";
|
|
1322
|
+
import path11 from "path";
|
|
1323
|
+
import chalk from "chalk";
|
|
984
1324
|
|
|
985
1325
|
// src/types.ts
|
|
986
1326
|
init_task();
|
|
987
1327
|
|
|
1328
|
+
// src/core/admin-token.ts
|
|
1329
|
+
init_node();
|
|
1330
|
+
import fs9 from "fs";
|
|
1331
|
+
import path9 from "path";
|
|
1332
|
+
function tokenFile() {
|
|
1333
|
+
return path9.join(task0Home(), "admin.token");
|
|
1334
|
+
}
|
|
1335
|
+
var cached = null;
|
|
1336
|
+
var AdminTokenUnavailableError = class extends Error {
|
|
1337
|
+
constructor() {
|
|
1338
|
+
super(
|
|
1339
|
+
`Admin token not found.
|
|
1340
|
+
\u2022 If the task0 server runs on this host, run the task0-server binary once \u2014 the token will be generated at ${tokenFile()}.
|
|
1341
|
+
\u2022 Otherwise, copy the token from the server host and set TASK0_ADMIN_TOKEN.`
|
|
1342
|
+
);
|
|
1343
|
+
this.name = "AdminTokenUnavailableError";
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
function readAdminToken() {
|
|
1347
|
+
if (cached) return cached;
|
|
1348
|
+
const fromEnv = process.env.TASK0_ADMIN_TOKEN?.trim();
|
|
1349
|
+
if (fromEnv) {
|
|
1350
|
+
cached = fromEnv;
|
|
1351
|
+
return cached;
|
|
1352
|
+
}
|
|
1353
|
+
const file = tokenFile();
|
|
1354
|
+
if (fs9.existsSync(file)) {
|
|
1355
|
+
const v = fs9.readFileSync(file, "utf-8").trim();
|
|
1356
|
+
if (v) {
|
|
1357
|
+
cached = v;
|
|
1358
|
+
return cached;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
throw new AdminTokenUnavailableError();
|
|
1362
|
+
}
|
|
1363
|
+
function adminAuthHeader() {
|
|
1364
|
+
return { authorization: `Bearer ${readAdminToken()}` };
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// src/core/daemon-config.ts
|
|
1368
|
+
init_node();
|
|
1369
|
+
import fs10 from "fs";
|
|
1370
|
+
import path10 from "path";
|
|
1371
|
+
function configDir() {
|
|
1372
|
+
return task0Home();
|
|
1373
|
+
}
|
|
1374
|
+
function configFile() {
|
|
1375
|
+
return path10.join(configDir(), "daemon.json");
|
|
1376
|
+
}
|
|
1377
|
+
function daemonConfigPath() {
|
|
1378
|
+
return configFile();
|
|
1379
|
+
}
|
|
1380
|
+
function readDaemonIdentity() {
|
|
1381
|
+
const file = configFile();
|
|
1382
|
+
if (!fs10.existsSync(file)) return null;
|
|
1383
|
+
try {
|
|
1384
|
+
const raw = fs10.readFileSync(file, "utf-8");
|
|
1385
|
+
return JSON.parse(raw);
|
|
1386
|
+
} catch {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
function writeDaemonIdentity(identity) {
|
|
1391
|
+
const file = configFile();
|
|
1392
|
+
fs10.mkdirSync(configDir(), { recursive: true });
|
|
1393
|
+
fs10.writeFileSync(file, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
1394
|
+
try {
|
|
1395
|
+
fs10.chmodSync(file, 384);
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
function clearDaemonIdentity() {
|
|
1400
|
+
const file = configFile();
|
|
1401
|
+
if (!fs10.existsSync(file)) return false;
|
|
1402
|
+
fs10.unlinkSync(file);
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// src/core/hub-client.ts
|
|
1407
|
+
var HubUnreachableError = class extends Error {
|
|
1408
|
+
constructor(url, cause) {
|
|
1409
|
+
super(`Cannot reach hub at ${url}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
1410
|
+
this.name = "HubUnreachableError";
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
var HubResponseError = class extends Error {
|
|
1414
|
+
status;
|
|
1415
|
+
code;
|
|
1416
|
+
body;
|
|
1417
|
+
constructor(status, body, code) {
|
|
1418
|
+
super(`Hub returned ${status}: ${body}`);
|
|
1419
|
+
this.name = "HubResponseError";
|
|
1420
|
+
this.status = status;
|
|
1421
|
+
this.code = code;
|
|
1422
|
+
this.body = body;
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
function resolveHubUrl() {
|
|
1426
|
+
const fromEnv = process.env.TASK0_API_URL?.trim();
|
|
1427
|
+
if (fromEnv) return fromEnv.replace(/\/$/, "");
|
|
1428
|
+
const identity = readDaemonIdentity();
|
|
1429
|
+
if (identity?.server_url) return identity.server_url.replace(/\/$/, "");
|
|
1430
|
+
return "http://127.0.0.1:4318";
|
|
1431
|
+
}
|
|
1432
|
+
function resolveAuthHeader() {
|
|
1433
|
+
const apiToken = process.env.TASK0_API_TOKEN?.trim();
|
|
1434
|
+
if (apiToken) return { authorization: `Bearer ${apiToken}` };
|
|
1435
|
+
try {
|
|
1436
|
+
return adminAuthHeader();
|
|
1437
|
+
} catch (error2) {
|
|
1438
|
+
if (error2 instanceof AdminTokenUnavailableError) {
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
`No CLI credential available. Set TASK0_API_TOKEN (preferred \u2014 issue one via the dashboard) or place a server admin token at ${error2.message.includes(".token") ? error2.message.split("at ")[1]?.split(".")[0] + ".token" : "~/.task0/admin.token"}.`
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
throw error2;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async function callHub(pathname, opts = {}) {
|
|
1447
|
+
const base = resolveHubUrl();
|
|
1448
|
+
const url = new URL(pathname.startsWith("/") ? pathname : `/${pathname}`, base + "/");
|
|
1449
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
1450
|
+
if (v !== void 0) url.searchParams.set(k, v);
|
|
1451
|
+
}
|
|
1452
|
+
const init = {
|
|
1453
|
+
method: opts.method ?? "GET",
|
|
1454
|
+
headers: {
|
|
1455
|
+
...resolveAuthHeader(),
|
|
1456
|
+
...opts.body !== void 0 ? { "content-type": "application/json" } : {}
|
|
1457
|
+
}
|
|
1458
|
+
};
|
|
1459
|
+
if (opts.body !== void 0) init.body = JSON.stringify(opts.body);
|
|
1460
|
+
if (opts.timeoutMs) init.signal = AbortSignal.timeout(opts.timeoutMs);
|
|
1461
|
+
let res;
|
|
1462
|
+
try {
|
|
1463
|
+
res = await fetch(url, init);
|
|
1464
|
+
} catch (error2) {
|
|
1465
|
+
throw new HubUnreachableError(url.toString(), error2);
|
|
1466
|
+
}
|
|
1467
|
+
if (!res.ok) {
|
|
1468
|
+
const body = await res.text().catch(() => "");
|
|
1469
|
+
let code = null;
|
|
1470
|
+
try {
|
|
1471
|
+
const parsed = JSON.parse(body);
|
|
1472
|
+
if (typeof parsed?.code === "string") code = parsed.code;
|
|
1473
|
+
} catch {
|
|
1474
|
+
}
|
|
1475
|
+
throw new HubResponseError(res.status, body, code);
|
|
1476
|
+
}
|
|
1477
|
+
if (res.status === 204) return void 0;
|
|
1478
|
+
const text = await res.text();
|
|
1479
|
+
if (!text) return void 0;
|
|
1480
|
+
return JSON.parse(text);
|
|
1481
|
+
}
|
|
1482
|
+
function localDaemonId() {
|
|
1483
|
+
const identity = readDaemonIdentity();
|
|
1484
|
+
return identity?.daemon_id ?? null;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
988
1487
|
// src/commands/source.ts
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1488
|
+
function reportHubError(error2) {
|
|
1489
|
+
if (error2 instanceof HubUnreachableError) {
|
|
1490
|
+
console.error(chalk.red(error2.message));
|
|
1491
|
+
console.error(chalk.dim("Tip: ensure the hub is running and TASK0_API_URL points to it."));
|
|
1492
|
+
process.exit(1);
|
|
1493
|
+
}
|
|
1494
|
+
if (error2 instanceof HubResponseError) {
|
|
1495
|
+
console.error(chalk.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
|
|
1496
|
+
process.exit(1);
|
|
1497
|
+
}
|
|
1498
|
+
throw error2;
|
|
1499
|
+
}
|
|
1500
|
+
function requireLocalDaemon() {
|
|
1501
|
+
const id = localDaemonId();
|
|
1502
|
+
if (!id) {
|
|
1503
|
+
console.error(chalk.red("This host is not registered as a daemon."));
|
|
1504
|
+
console.error(chalk.dim("Run `task0 daemon register --server <url>` first."));
|
|
996
1505
|
process.exit(1);
|
|
997
1506
|
}
|
|
998
|
-
|
|
1507
|
+
return id;
|
|
1508
|
+
}
|
|
1509
|
+
var source = new Command("source").description("Manage task sources");
|
|
1510
|
+
source.command("add <path>").description("Register a local project on this host's daemon (via hub)").option("-n, --name <name>", "Source name (defaults to directory name)").action(async (inputPath, opts) => {
|
|
1511
|
+
const absPath = path11.resolve(inputPath);
|
|
1512
|
+
const name = opts.name || path11.basename(absPath);
|
|
1513
|
+
const daemonId = requireLocalDaemon();
|
|
1514
|
+
let resp;
|
|
1515
|
+
try {
|
|
1516
|
+
resp = await callHub(`/api/daemons/${encodeURIComponent(daemonId)}/projects`, {
|
|
1517
|
+
method: "POST",
|
|
1518
|
+
body: { path: absPath, name }
|
|
1519
|
+
});
|
|
1520
|
+
} catch (error2) {
|
|
1521
|
+
reportHubError(error2);
|
|
1522
|
+
}
|
|
999
1523
|
console.log(chalk.green(`Added source "${name}" \u2192 ${absPath}`));
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
for (const err of result.errors) console.warn(chalk.yellow(` warn: ${err}`));
|
|
1524
|
+
if (typeof resp.taskCount === "number") {
|
|
1525
|
+
console.log(` ${resp.taskCount} tasks found`);
|
|
1003
1526
|
}
|
|
1004
1527
|
});
|
|
1005
|
-
source.command("list").description("List registered task sources").action(() => {
|
|
1006
|
-
|
|
1007
|
-
|
|
1528
|
+
source.command("list").description("List registered task sources (queries the hub)").action(async () => {
|
|
1529
|
+
let data;
|
|
1530
|
+
try {
|
|
1531
|
+
data = await callHub("/api/config");
|
|
1532
|
+
} catch (error2) {
|
|
1533
|
+
if (error2 instanceof HubUnreachableError) {
|
|
1534
|
+
console.error(chalk.red(error2.message));
|
|
1535
|
+
console.error(chalk.dim("Tip: ensure the hub is running and TASK0_API_URL points to it."));
|
|
1536
|
+
process.exit(1);
|
|
1537
|
+
}
|
|
1538
|
+
if (error2 instanceof HubResponseError) {
|
|
1539
|
+
console.error(chalk.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
|
|
1540
|
+
process.exit(1);
|
|
1541
|
+
}
|
|
1542
|
+
throw error2;
|
|
1543
|
+
}
|
|
1544
|
+
if (data.sources.length === 0) {
|
|
1008
1545
|
console.log("No sources registered. Use `task0 source add <path>` to add one.");
|
|
1009
1546
|
return;
|
|
1010
1547
|
}
|
|
1011
|
-
for (const s of
|
|
1548
|
+
for (const s of data.sources) {
|
|
1012
1549
|
const status = s.enabled ? chalk.green("\u25CF") : chalk.dim("\u25CB");
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
});
|
|
1016
|
-
source.command("remove <name>").description("Remove a task source").action((name) => {
|
|
1017
|
-
if (removeSource(name)) {
|
|
1018
|
-
console.log(chalk.green(`Removed source "${name}"`));
|
|
1019
|
-
} else {
|
|
1020
|
-
console.error(chalk.red(`Source "${name}" not found`));
|
|
1021
|
-
process.exit(1);
|
|
1550
|
+
const where = s.path ?? (s.type === "github" ? "(github)" : s.type === "linear" ? "(linear)" : "");
|
|
1551
|
+
const owner = s.daemon_id ? chalk.dim(` @${s.daemon_id}`) : "";
|
|
1552
|
+
console.log(`${status} ${s.name} ${chalk.dim(s.type)} ${where}${owner}`);
|
|
1022
1553
|
}
|
|
1023
1554
|
});
|
|
1024
|
-
source.command("
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1555
|
+
source.command("remove <name>").description("Remove a project source from this host's daemon (via hub)").action(async (name) => {
|
|
1556
|
+
const daemonId = requireLocalDaemon();
|
|
1557
|
+
try {
|
|
1558
|
+
await callHub(`/api/daemons/${encodeURIComponent(daemonId)}/projects/${encodeURIComponent(name)}`, {
|
|
1559
|
+
method: "DELETE"
|
|
1560
|
+
});
|
|
1561
|
+
} catch (error2) {
|
|
1562
|
+
if (error2 instanceof HubResponseError && error2.status === 404) {
|
|
1563
|
+
console.error(chalk.red(`Source "${name}" not found on this daemon.`));
|
|
1030
1564
|
process.exit(1);
|
|
1031
1565
|
}
|
|
1032
|
-
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1566
|
+
reportHubError(error2);
|
|
1567
|
+
}
|
|
1568
|
+
console.log(chalk.green(`Removed source "${name}"`));
|
|
1569
|
+
});
|
|
1570
|
+
source.command("scan [name]").description("Scan source(s) via the hub and display tasks").option("--json", "Output as JSON").action(async (name, opts) => {
|
|
1571
|
+
let data;
|
|
1572
|
+
try {
|
|
1573
|
+
data = await callHub("/api/tasks", { query: { source: name } });
|
|
1574
|
+
} catch (error2) {
|
|
1575
|
+
reportHubError(error2);
|
|
1576
|
+
}
|
|
1577
|
+
if (data.tasks.length === 0) {
|
|
1578
|
+
console.log(name ? `No tasks in source "${name}".` : "No tasks.");
|
|
1036
1579
|
return;
|
|
1037
1580
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1581
|
+
if (opts.json) {
|
|
1582
|
+
const out = data.tasks.map((t) => ({ ...t, _source: t.project }));
|
|
1583
|
+
console.log(JSON.stringify(out, null, 2));
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
1587
|
+
for (const t of data.tasks) {
|
|
1588
|
+
const list = byProject.get(t.project) ?? [];
|
|
1589
|
+
list.push(t);
|
|
1590
|
+
byProject.set(t.project, list);
|
|
1591
|
+
}
|
|
1592
|
+
for (const [project2, tasks] of byProject) {
|
|
1593
|
+
console.log(chalk.bold(`
|
|
1594
|
+
${project2}`));
|
|
1595
|
+
const objectIdWidth = Math.max(...tasks.map((t) => (t.object_id || "").length), 9);
|
|
1596
|
+
for (const t of tasks) {
|
|
1597
|
+
const statusColor2 = isActiveTaskStatus(t.status) ? chalk.green : t.status === "blocked" ? chalk.red : t.status === "todo" ? chalk.yellow : t.status === "done" ? chalk.dim : chalk.white;
|
|
1598
|
+
const objectId = chalk.cyan((t.object_id || "-").padEnd(objectIdWidth));
|
|
1599
|
+
console.log(` ${objectId} ${statusColor2(t.status.padEnd(8))} ${chalk.dim(t.id)} ${t.title}`);
|
|
1056
1600
|
}
|
|
1601
|
+
console.log(chalk.dim(` ${tasks.length} tasks`));
|
|
1057
1602
|
}
|
|
1058
|
-
if (
|
|
1059
|
-
console.
|
|
1603
|
+
if (data.errors.length > 0) {
|
|
1604
|
+
for (const err of data.errors) console.warn(chalk.yellow(`warn: ${err}`));
|
|
1060
1605
|
}
|
|
1061
1606
|
});
|
|
1062
1607
|
|
|
1063
1608
|
// src/commands/project.ts
|
|
1064
1609
|
import { Command as Command2 } from "commander";
|
|
1065
|
-
import
|
|
1066
|
-
import
|
|
1067
|
-
import
|
|
1610
|
+
import fs11 from "fs";
|
|
1611
|
+
import path12 from "path";
|
|
1612
|
+
import yaml5 from "js-yaml";
|
|
1068
1613
|
import chalk2 from "chalk";
|
|
1069
1614
|
|
|
1070
1615
|
// ../../packages/shared/dist/index.js
|
|
@@ -1072,44 +1617,45 @@ init_object_id();
|
|
|
1072
1617
|
|
|
1073
1618
|
// src/commands/project.ts
|
|
1074
1619
|
var project = new Command2("project").description("Manage projects");
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
if (!fs8.existsSync(ymlPath)) return "-";
|
|
1620
|
+
project.command("list").description("List registered projects (queries the hub)").action(async () => {
|
|
1621
|
+
let data;
|
|
1078
1622
|
try {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1623
|
+
data = await callHub("/api/config");
|
|
1624
|
+
} catch (error2) {
|
|
1625
|
+
if (error2 instanceof HubUnreachableError) {
|
|
1626
|
+
console.error(chalk2.red(error2.message));
|
|
1627
|
+
process.exit(1);
|
|
1628
|
+
}
|
|
1629
|
+
if (error2 instanceof HubResponseError) {
|
|
1630
|
+
console.error(chalk2.red(`Hub error (${error2.status}): ${error2.body || "(empty body)"}`));
|
|
1631
|
+
process.exit(1);
|
|
1632
|
+
}
|
|
1633
|
+
throw error2;
|
|
1084
1634
|
}
|
|
1085
|
-
|
|
1086
|
-
project.command("list").description("List registered projects").action(() => {
|
|
1087
|
-
const projects = loadConfig().sources.filter((s) => s.type === "project");
|
|
1635
|
+
const projects = data.sources.filter((s) => s.type === "project");
|
|
1088
1636
|
if (projects.length === 0) {
|
|
1089
1637
|
console.log("No projects registered. Use `task0 source add <path>` to add one.");
|
|
1090
1638
|
return;
|
|
1091
1639
|
}
|
|
1092
|
-
const
|
|
1093
|
-
const
|
|
1094
|
-
const nameWidth = Math.max(...rows.map((r) => r.p.name.length), 4);
|
|
1095
|
-
for (const { p, objectId } of rows) {
|
|
1640
|
+
const nameWidth = Math.max(...projects.map((p) => p.name.length), 4);
|
|
1641
|
+
for (const p of projects) {
|
|
1096
1642
|
const status = p.enabled ? chalk2.green("\u25CF") : chalk2.dim("\u25CB");
|
|
1097
|
-
const oid = chalk2.cyan(objectId.padEnd(objectIdWidth));
|
|
1098
1643
|
const name = p.name.padEnd(nameWidth);
|
|
1099
|
-
|
|
1644
|
+
const owner = p.daemon_id ? chalk2.dim(` @${p.daemon_id}`) : "";
|
|
1645
|
+
console.log(`${status} ${name} ${chalk2.dim(p.path ?? "")}${owner}`);
|
|
1100
1646
|
}
|
|
1101
1647
|
});
|
|
1102
1648
|
project.command("init").description("Initialize task0.yml in the current directory").option("-d, --tasks-dir <dir>", "Tasks directory", ".task0/tasks").action((opts) => {
|
|
1103
1649
|
const cwd = process.cwd();
|
|
1104
|
-
const ymlPath =
|
|
1105
|
-
if (
|
|
1650
|
+
const ymlPath = path12.join(cwd, "task0.yml");
|
|
1651
|
+
if (fs11.existsSync(ymlPath)) {
|
|
1106
1652
|
console.error(chalk2.yellow("task0.yml already exists"));
|
|
1107
1653
|
process.exit(1);
|
|
1108
1654
|
}
|
|
1109
1655
|
const config = { kind: "project", object_id: generateObjectId("project"), tasks_dir: opts.tasksDir };
|
|
1110
|
-
|
|
1111
|
-
const tasksDir =
|
|
1112
|
-
|
|
1656
|
+
fs11.writeFileSync(ymlPath, yaml5.dump(config), "utf-8");
|
|
1657
|
+
const tasksDir = path12.join(cwd, opts.tasksDir);
|
|
1658
|
+
fs11.mkdirSync(tasksDir, { recursive: true });
|
|
1113
1659
|
console.log(chalk2.green("Initialized task0 project"));
|
|
1114
1660
|
console.log(` ${ymlPath}`);
|
|
1115
1661
|
console.log(` ${tasksDir}/`);
|
|
@@ -1118,61 +1664,57 @@ project.command("init").description("Initialize task0.yml in the current directo
|
|
|
1118
1664
|
// src/commands/task.ts
|
|
1119
1665
|
import { Command as Command8 } from "commander";
|
|
1120
1666
|
import { execSync } from "child_process";
|
|
1121
|
-
import
|
|
1122
|
-
import
|
|
1123
|
-
import
|
|
1667
|
+
import fs17 from "fs";
|
|
1668
|
+
import path15 from "path";
|
|
1669
|
+
import yaml6 from "js-yaml";
|
|
1124
1670
|
import chalk8 from "chalk";
|
|
1125
1671
|
|
|
1126
1672
|
// src/lib/api.ts
|
|
1127
|
-
var DEFAULT_BASE = "http://127.0.0.1:4318";
|
|
1128
1673
|
function apiBaseUrl() {
|
|
1129
|
-
return process.env.TASK0_API_URL ||
|
|
1674
|
+
return process.env.TASK0_API_URL || "http://127.0.0.1:4318";
|
|
1130
1675
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
let res;
|
|
1134
|
-
try {
|
|
1135
|
-
res = await fetch(url, {
|
|
1136
|
-
method,
|
|
1137
|
-
headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
|
|
1138
|
-
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
1139
|
-
});
|
|
1140
|
-
} catch (error2) {
|
|
1676
|
+
function toApiError(method, pathname, error2) {
|
|
1677
|
+
if (error2 instanceof HubUnreachableError) {
|
|
1141
1678
|
const err = new Error(
|
|
1142
1679
|
`Cannot reach task0 API at ${apiBaseUrl()}. Start the task0-server binary (download from GitHub Releases) or repoint TASK0_API_URL.`
|
|
1143
1680
|
);
|
|
1144
1681
|
err.cause = error2;
|
|
1145
|
-
|
|
1682
|
+
return err;
|
|
1146
1683
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1684
|
+
if (error2 instanceof HubResponseError) {
|
|
1685
|
+
let parsedBody = error2.body;
|
|
1686
|
+
try {
|
|
1687
|
+
parsedBody = error2.body ? JSON.parse(error2.body) : null;
|
|
1688
|
+
} catch {
|
|
1689
|
+
}
|
|
1690
|
+
const message = parsedBody?.error || error2.body || error2.message;
|
|
1691
|
+
const err = new Error(`API ${method} ${pathname} failed (${error2.status}): ${message}`);
|
|
1692
|
+
err.status = error2.status;
|
|
1693
|
+
err.body = parsedBody;
|
|
1694
|
+
return err;
|
|
1153
1695
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1696
|
+
return error2 instanceof Error ? error2 : new Error(String(error2));
|
|
1697
|
+
}
|
|
1698
|
+
async function request(method, pathname, body) {
|
|
1699
|
+
try {
|
|
1700
|
+
return await callHub(pathname, { method, body });
|
|
1701
|
+
} catch (error2) {
|
|
1702
|
+
throw toApiError(method, pathname, error2);
|
|
1160
1703
|
}
|
|
1161
|
-
return parsed;
|
|
1162
1704
|
}
|
|
1163
1705
|
var api = {
|
|
1164
|
-
get: (
|
|
1165
|
-
post: (
|
|
1166
|
-
put: (
|
|
1167
|
-
patch: (
|
|
1168
|
-
del: (
|
|
1706
|
+
get: (path31) => request("GET", path31),
|
|
1707
|
+
post: (path31, body) => request("POST", path31, body ?? {}),
|
|
1708
|
+
put: (path31, body) => request("PUT", path31, body ?? {}),
|
|
1709
|
+
patch: (path31, body) => request("PATCH", path31, body ?? {}),
|
|
1710
|
+
del: (path31) => request("DELETE", path31)
|
|
1169
1711
|
};
|
|
1170
1712
|
|
|
1171
1713
|
// src/commands/task/triage.ts
|
|
1172
1714
|
import { Command as Command3 } from "commander";
|
|
1173
1715
|
import chalk3 from "chalk";
|
|
1174
|
-
import
|
|
1175
|
-
import
|
|
1716
|
+
import fs13 from "fs";
|
|
1717
|
+
import path13 from "path";
|
|
1176
1718
|
|
|
1177
1719
|
// src/core/agent-run-wait.ts
|
|
1178
1720
|
async function getAgentRun(id) {
|
|
@@ -1223,8 +1765,8 @@ init_task_state2();
|
|
|
1223
1765
|
var ISSUE_DETAIL_RE = /^ISSUE-\d+\.md$/;
|
|
1224
1766
|
var TRIAGE_SKILL_NAME = "triage";
|
|
1225
1767
|
function resolveSkillFilePath(projectRoot, skillName) {
|
|
1226
|
-
const p =
|
|
1227
|
-
return
|
|
1768
|
+
const p = path13.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
|
|
1769
|
+
return fs13.existsSync(p) ? p : null;
|
|
1228
1770
|
}
|
|
1229
1771
|
var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md + ISSUE-NN.md").argument("<objectId>", "Task object_id (tsk_XXXXX)").option("--agent <name>", "Agent (claude-code|codex)", "claude-code").option("--model <id>", "Model id or alias (see `task0 models refresh`)").option("--effort <level>", "Reasoning effort (e.g. low|medium|high)").option("--idea <file>", "IDEA file (default: latest IDEA-NN.md)").option("--force", "Overwrite existing ISSUE files").option("--wait", "Wait for completion").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
1230
1772
|
try {
|
|
@@ -1243,7 +1785,7 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
|
|
|
1243
1785
|
process.exit(1);
|
|
1244
1786
|
}
|
|
1245
1787
|
for (const name of existingIssues) {
|
|
1246
|
-
|
|
1788
|
+
fs13.rmSync(path13.join(loc.taskDir, name), { force: true });
|
|
1247
1789
|
}
|
|
1248
1790
|
}
|
|
1249
1791
|
if (opts.model || opts.effort) {
|
|
@@ -1281,8 +1823,8 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
|
|
|
1281
1823
|
console.error(chalk3.red(`triage failed: ${final.error || "unknown"}`));
|
|
1282
1824
|
process.exit(1);
|
|
1283
1825
|
}
|
|
1284
|
-
const hasOverview =
|
|
1285
|
-
const issueFiles =
|
|
1826
|
+
const hasOverview = fs13.existsSync(path13.join(loc.taskDir, "ISSUE.md"));
|
|
1827
|
+
const issueFiles = fs13.readdirSync(loc.taskDir).filter((name) => ISSUE_DETAIL_RE.test(name)).sort();
|
|
1286
1828
|
if (!hasOverview || issueFiles.length === 0) {
|
|
1287
1829
|
const missing = [];
|
|
1288
1830
|
if (!hasOverview) missing.push("ISSUE.md");
|
|
@@ -1292,7 +1834,7 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
|
|
|
1292
1834
|
}
|
|
1293
1835
|
let blockingQuestionCount = 0;
|
|
1294
1836
|
for (const name of issueFiles) {
|
|
1295
|
-
const content =
|
|
1837
|
+
const content = fs13.readFileSync(path13.join(loc.taskDir, name), "utf-8");
|
|
1296
1838
|
blockingQuestionCount += countBlockingQuestions(content);
|
|
1297
1839
|
}
|
|
1298
1840
|
await updateWorkflow(loc.taskYml, {
|
|
@@ -1320,8 +1862,8 @@ var triage = new Command3("triage").description("Decompose IDEA into ISSUE.md +
|
|
|
1320
1862
|
}
|
|
1321
1863
|
});
|
|
1322
1864
|
function listIssueArtifacts(taskDir) {
|
|
1323
|
-
if (!
|
|
1324
|
-
return
|
|
1865
|
+
if (!fs13.existsSync(taskDir)) return [];
|
|
1866
|
+
return fs13.readdirSync(taskDir).filter((name) => name === "ISSUE.md" || ISSUE_DETAIL_RE.test(name)).sort();
|
|
1325
1867
|
}
|
|
1326
1868
|
function countBlockingQuestions(md) {
|
|
1327
1869
|
const match = md.match(/## Open Questions\s*\n([\s\S]*?)(\n## |\n*$)/i);
|
|
@@ -1335,13 +1877,13 @@ function countBlockingQuestions(md) {
|
|
|
1335
1877
|
// src/commands/task/exec.ts
|
|
1336
1878
|
import { Command as Command4 } from "commander";
|
|
1337
1879
|
import chalk4 from "chalk";
|
|
1338
|
-
import
|
|
1339
|
-
import
|
|
1880
|
+
import fs14 from "fs";
|
|
1881
|
+
import path14 from "path";
|
|
1340
1882
|
init_task_state2();
|
|
1341
1883
|
var PLAN_EXECUTE_SKILL_NAME = "plan-execute";
|
|
1342
1884
|
function resolveSkillFilePath2(projectRoot, skillName) {
|
|
1343
|
-
const p =
|
|
1344
|
-
return
|
|
1885
|
+
const p = path14.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
|
|
1886
|
+
return fs14.existsSync(p) ? p : null;
|
|
1345
1887
|
}
|
|
1346
1888
|
var exec = new Command4("exec").description("Execute a plan against the task (cwd = project root; agent sets up its own worktree if needed)").argument("<objectId>", "Task object_id (tsk_XXXXX)").option("--agent <name>", "Agent (claude-code|codex|cursor)", "claude-code").option("--model <id>", "Model id or alias (see `task0 models refresh`)").option("--effort <level>", "Reasoning effort (e.g. low|medium|high)").option("--plan <file>", "Plan file (default: refined plan, else latest PLAN)").option("--no-commit", "Skip commit").option("--wait", "Wait for completion").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
1347
1889
|
try {
|
|
@@ -1434,7 +1976,7 @@ var summarize = new Command5("summarize").description("Generate a concise title
|
|
|
1434
1976
|
});
|
|
1435
1977
|
|
|
1436
1978
|
// src/commands/task/comment.ts
|
|
1437
|
-
import
|
|
1979
|
+
import fs15 from "fs";
|
|
1438
1980
|
import { Command as Command6 } from "commander";
|
|
1439
1981
|
import chalk6 from "chalk";
|
|
1440
1982
|
var comment = new Command6("comment").description("Manage comments on a task");
|
|
@@ -1444,8 +1986,8 @@ function fail(err) {
|
|
|
1444
1986
|
}
|
|
1445
1987
|
function readBodyFromOpts(opts) {
|
|
1446
1988
|
if (opts.body !== void 0) return opts.body;
|
|
1447
|
-
if (opts.file === "-") return
|
|
1448
|
-
if (opts.file) return
|
|
1989
|
+
if (opts.file === "-") return fs15.readFileSync(0, "utf-8");
|
|
1990
|
+
if (opts.file) return fs15.readFileSync(opts.file, "utf-8");
|
|
1449
1991
|
throw new Error("Provide --body or --file");
|
|
1450
1992
|
}
|
|
1451
1993
|
function preview(body, width = 60) {
|
|
@@ -1544,7 +2086,7 @@ comment.command("delete <cmtId>").description("Delete a comment by its cmt_ id")
|
|
|
1544
2086
|
});
|
|
1545
2087
|
|
|
1546
2088
|
// src/commands/task/description.ts
|
|
1547
|
-
import
|
|
2089
|
+
import fs16 from "fs";
|
|
1548
2090
|
import { Command as Command7 } from "commander";
|
|
1549
2091
|
import chalk7 from "chalk";
|
|
1550
2092
|
var description = new Command7("description").description("Show or update the task description");
|
|
@@ -1554,8 +2096,8 @@ function fail2(err) {
|
|
|
1554
2096
|
}
|
|
1555
2097
|
function readBodyFromOpts2(opts) {
|
|
1556
2098
|
if (opts.body !== void 0) return opts.body;
|
|
1557
|
-
if (opts.file === "-") return
|
|
1558
|
-
if (opts.file) return
|
|
2099
|
+
if (opts.file === "-") return fs16.readFileSync(0, "utf-8");
|
|
2100
|
+
if (opts.file) return fs16.readFileSync(opts.file, "utf-8");
|
|
1559
2101
|
throw new Error("Provide --body or --file (use --file - to read stdin)");
|
|
1560
2102
|
}
|
|
1561
2103
|
description.command("show <taskId>").description("Print the current description (taskId is short id or tsk_)").option("--json", "Output JSON").action(async (taskId, opts) => {
|
|
@@ -1604,12 +2146,12 @@ task.addCommand(comment);
|
|
|
1604
2146
|
task.addCommand(description);
|
|
1605
2147
|
task.command("init <input>").description("Create a task from a description or Linear/GitHub issue URL").action(async (input) => {
|
|
1606
2148
|
const cwd = process.cwd();
|
|
1607
|
-
const projectYml =
|
|
1608
|
-
if (!
|
|
2149
|
+
const projectYml = path15.join(cwd, "task0.yml");
|
|
2150
|
+
if (!fs17.existsSync(projectYml)) {
|
|
1609
2151
|
console.error(chalk8.red("Not a task0 project (task0.yml not found). Run `task0 project init` first."));
|
|
1610
2152
|
process.exit(1);
|
|
1611
2153
|
}
|
|
1612
|
-
const projectConfig =
|
|
2154
|
+
const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
|
|
1613
2155
|
if (projectConfig.kind !== "project") {
|
|
1614
2156
|
console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
|
|
1615
2157
|
process.exit(1);
|
|
@@ -1625,38 +2167,38 @@ task.command("init <input>").description("Create a task from a description or Li
|
|
|
1625
2167
|
});
|
|
1626
2168
|
task.command("migrate").description("Add task0.yml to legacy task directories that lack one").option("--dry-run", "Show what would be created without writing").action((opts) => {
|
|
1627
2169
|
const cwd = process.cwd();
|
|
1628
|
-
const projectYml =
|
|
1629
|
-
if (!
|
|
2170
|
+
const projectYml = path15.join(cwd, "task0.yml");
|
|
2171
|
+
if (!fs17.existsSync(projectYml)) {
|
|
1630
2172
|
console.error(chalk8.red("Not a task0 project (task0.yml not found). Run `task0 project init` first."));
|
|
1631
2173
|
process.exit(1);
|
|
1632
2174
|
}
|
|
1633
|
-
const projectConfig =
|
|
2175
|
+
const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
|
|
1634
2176
|
if (projectConfig.kind !== "project") {
|
|
1635
2177
|
console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
|
|
1636
2178
|
process.exit(1);
|
|
1637
2179
|
}
|
|
1638
|
-
const tasksDir =
|
|
1639
|
-
if (!
|
|
2180
|
+
const tasksDir = path15.join(cwd, projectConfig.tasks_dir);
|
|
2181
|
+
if (!fs17.existsSync(tasksDir)) {
|
|
1640
2182
|
console.error(chalk8.red(`Tasks directory not found: ${tasksDir}`));
|
|
1641
2183
|
process.exit(1);
|
|
1642
2184
|
}
|
|
1643
|
-
const entries =
|
|
2185
|
+
const entries = fs17.readdirSync(tasksDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1644
2186
|
let migrated = 0;
|
|
1645
2187
|
let skipped = 0;
|
|
1646
2188
|
let seededObjectIds = 0;
|
|
1647
2189
|
for (const name of entries) {
|
|
1648
|
-
const taskDir =
|
|
1649
|
-
const taskYml =
|
|
1650
|
-
if (
|
|
2190
|
+
const taskDir = path15.join(tasksDir, name);
|
|
2191
|
+
const taskYml = path15.join(taskDir, "task0.yml");
|
|
2192
|
+
if (fs17.existsSync(taskYml)) {
|
|
1651
2193
|
skipped++;
|
|
1652
2194
|
try {
|
|
1653
|
-
const raw =
|
|
2195
|
+
const raw = yaml6.load(fs17.readFileSync(taskYml, "utf-8"));
|
|
1654
2196
|
if (raw && raw.kind === "task" && !raw.object_id) {
|
|
1655
2197
|
raw.object_id = generateObjectId("task");
|
|
1656
2198
|
if (opts.dryRun) {
|
|
1657
2199
|
console.log(chalk8.dim(`[dry-run] seed object_id: ${taskYml}`));
|
|
1658
2200
|
} else {
|
|
1659
|
-
|
|
2201
|
+
fs17.writeFileSync(taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
|
|
1660
2202
|
console.log(chalk8.green(` seed object_id: ${taskYml}`));
|
|
1661
2203
|
}
|
|
1662
2204
|
seededObjectIds++;
|
|
@@ -1681,7 +2223,7 @@ task.command("migrate").description("Add task0.yml to legacy task directories th
|
|
|
1681
2223
|
if (opts.dryRun) {
|
|
1682
2224
|
console.log(chalk8.dim(`[dry-run] ${taskYml}`));
|
|
1683
2225
|
} else {
|
|
1684
|
-
|
|
2226
|
+
fs17.writeFileSync(taskYml, yaml6.dump(taskConfig), "utf-8");
|
|
1685
2227
|
console.log(chalk8.green(` ${taskYml}`));
|
|
1686
2228
|
}
|
|
1687
2229
|
migrated++;
|
|
@@ -1741,7 +2283,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
|
|
|
1741
2283
|
try {
|
|
1742
2284
|
const { resolveTaskByObjectId: resolveTaskByObjectId2 } = await Promise.resolve().then(() => (init_task_state2(), task_state_exports));
|
|
1743
2285
|
const loc = resolveTaskByObjectId2(id);
|
|
1744
|
-
const raw =
|
|
2286
|
+
const raw = yaml6.load(fs17.readFileSync(loc.taskYml, "utf-8"));
|
|
1745
2287
|
if (opts.phase) {
|
|
1746
2288
|
const phase = opts.phase.trim();
|
|
1747
2289
|
if (!phase) {
|
|
@@ -1754,7 +2296,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
|
|
|
1754
2296
|
return;
|
|
1755
2297
|
}
|
|
1756
2298
|
raw.workflow = { ...workflow2, phase };
|
|
1757
|
-
|
|
2299
|
+
fs17.writeFileSync(loc.taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
|
|
1758
2300
|
console.log(chalk8.green(`${id} phase: ${phase}`));
|
|
1759
2301
|
return;
|
|
1760
2302
|
}
|
|
@@ -1765,7 +2307,7 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
|
|
|
1765
2307
|
raw.status = "done";
|
|
1766
2308
|
const workflow = raw.workflow ?? {};
|
|
1767
2309
|
raw.workflow = { ...workflow, phase: "completed" };
|
|
1768
|
-
|
|
2310
|
+
fs17.writeFileSync(loc.taskYml, yaml6.dump(raw, { lineWidth: 120 }), "utf-8");
|
|
1769
2311
|
console.log(chalk8.green(`${id} marked as done`));
|
|
1770
2312
|
} catch (err) {
|
|
1771
2313
|
console.error(chalk8.red(err.message));
|
|
@@ -1774,33 +2316,33 @@ task.command("done <id>").description("Write task lifecycle state to task0.yml \
|
|
|
1774
2316
|
});
|
|
1775
2317
|
task.command("archive <id>").description("Archive a task (append to tasks.tar, remove from tasks/)").action((id) => {
|
|
1776
2318
|
const cwd = process.cwd();
|
|
1777
|
-
const projectYml =
|
|
1778
|
-
if (!
|
|
2319
|
+
const projectYml = path15.join(cwd, "task0.yml");
|
|
2320
|
+
if (!fs17.existsSync(projectYml)) {
|
|
1779
2321
|
console.error(chalk8.red("Not a task0 project (task0.yml not found)."));
|
|
1780
2322
|
process.exit(1);
|
|
1781
2323
|
}
|
|
1782
|
-
const projectConfig =
|
|
2324
|
+
const projectConfig = yaml6.load(fs17.readFileSync(projectYml, "utf-8"));
|
|
1783
2325
|
if (projectConfig.kind !== "project") {
|
|
1784
2326
|
console.error(chalk8.red('Invalid task0.yml: kind is not "project"'));
|
|
1785
2327
|
process.exit(1);
|
|
1786
2328
|
}
|
|
1787
|
-
const tasksDir =
|
|
1788
|
-
const taskDir =
|
|
1789
|
-
if (!
|
|
2329
|
+
const tasksDir = path15.join(cwd, projectConfig.tasks_dir);
|
|
2330
|
+
const taskDir = path15.join(tasksDir, id);
|
|
2331
|
+
if (!fs17.existsSync(taskDir)) {
|
|
1790
2332
|
console.error(chalk8.red(`Task "${id}" not found at ${taskDir}`));
|
|
1791
2333
|
process.exit(1);
|
|
1792
2334
|
}
|
|
1793
2335
|
const tarFile = tasksDir + ".tar";
|
|
1794
2336
|
archiveTaskToTar(tasksDir, id, tarFile);
|
|
1795
|
-
|
|
2337
|
+
fs17.rmSync(taskDir, { recursive: true });
|
|
1796
2338
|
console.log(chalk8.green(`Archived "${id}" \u2192 ${tarFile}`));
|
|
1797
2339
|
});
|
|
1798
2340
|
function archiveTaskToTar(tasksDir, taskId, tarFile) {
|
|
1799
|
-
if (
|
|
2341
|
+
if (fs17.existsSync(tarFile) && !fs17.statSync(tarFile).isFile()) {
|
|
1800
2342
|
console.error(chalk8.red(`${tarFile} exists but is not a file (likely a leftover directory). Remove it manually first.`));
|
|
1801
2343
|
process.exit(1);
|
|
1802
2344
|
}
|
|
1803
|
-
if (
|
|
2345
|
+
if (fs17.existsSync(tarFile)) {
|
|
1804
2346
|
execSync(`tar -rf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksDir)} ${JSON.stringify(taskId)}`);
|
|
1805
2347
|
} else {
|
|
1806
2348
|
execSync(`tar -cf ${JSON.stringify(tarFile)} -C ${JSON.stringify(tasksDir)} ${JSON.stringify(taskId)}`);
|
|
@@ -1810,9 +2352,9 @@ function archiveTaskToTar(tasksDir, taskId, tarFile) {
|
|
|
1810
2352
|
// src/commands/ui.ts
|
|
1811
2353
|
import { Command as Command9 } from "commander";
|
|
1812
2354
|
import { spawn } from "child_process";
|
|
1813
|
-
import
|
|
2355
|
+
import path16 from "path";
|
|
1814
2356
|
import chalk9 from "chalk";
|
|
1815
|
-
var DASHBOARD_DIR =
|
|
2357
|
+
var DASHBOARD_DIR = path16.resolve(
|
|
1816
2358
|
import.meta.dirname,
|
|
1817
2359
|
"..",
|
|
1818
2360
|
"..",
|
|
@@ -1846,17 +2388,17 @@ import chalk10 from "chalk";
|
|
|
1846
2388
|
// ../../packages/shared/dist/types/agent.js
|
|
1847
2389
|
var AGENT_KINDS = ["coding", "llm_api", "workflow"];
|
|
1848
2390
|
var AGENT_KIND_SET = new Set(AGENT_KINDS);
|
|
1849
|
-
var
|
|
1850
|
-
var
|
|
1851
|
-
function
|
|
1852
|
-
return typeof value === "string" &&
|
|
2391
|
+
var AGENT_PROVIDERS = ["claude-code", "codex", "cursor-agent"];
|
|
2392
|
+
var AGENT_PROVIDER_SET = new Set(AGENT_PROVIDERS);
|
|
2393
|
+
function isAgentProvider(value) {
|
|
2394
|
+
return typeof value === "string" && AGENT_PROVIDER_SET.has(value);
|
|
1853
2395
|
}
|
|
1854
2396
|
|
|
1855
2397
|
// ../../packages/shared/dist/types/runtime.js
|
|
1856
2398
|
var AGENT_RUN_STATUS_VALUES = ["starting", "running", "done", "error"];
|
|
1857
2399
|
var AGENT_RUN_STATUS_SET = new Set(AGENT_RUN_STATUS_VALUES);
|
|
1858
|
-
var RUNTIME_AGENTS =
|
|
1859
|
-
var isRuntimeAgent =
|
|
2400
|
+
var RUNTIME_AGENTS = AGENT_PROVIDERS;
|
|
2401
|
+
var isRuntimeAgent = isAgentProvider;
|
|
1860
2402
|
var AGENT_MODEL_DEFAULTS = {
|
|
1861
2403
|
"claude-code": [
|
|
1862
2404
|
{ id: "opus", label: "Opus" },
|
|
@@ -1868,20 +2410,45 @@ var AGENT_MODEL_DEFAULTS = {
|
|
|
1868
2410
|
{ id: "o3", label: "o3" },
|
|
1869
2411
|
{ id: "o4-mini", label: "o4-mini" }
|
|
1870
2412
|
],
|
|
1871
|
-
cursor: [
|
|
2413
|
+
"cursor-agent": [
|
|
1872
2414
|
{ id: "", label: "Default" },
|
|
1873
2415
|
{ id: "gpt-5", label: "GPT-5" },
|
|
1874
2416
|
{ id: "sonnet-4", label: "Sonnet 4" },
|
|
1875
2417
|
{ id: "sonnet-4-thinking", label: "Sonnet 4 Thinking" }
|
|
1876
2418
|
]
|
|
1877
2419
|
};
|
|
2420
|
+
var AGENT_EFFORT_DEFAULTS = {
|
|
2421
|
+
"claude-code": [
|
|
2422
|
+
{ id: "high", label: "High" },
|
|
2423
|
+
{ id: "max", label: "Max" },
|
|
2424
|
+
{ id: "medium", label: "Medium" },
|
|
2425
|
+
{ id: "low", label: "Low" }
|
|
2426
|
+
],
|
|
2427
|
+
codex: [
|
|
2428
|
+
{ id: "xhigh", label: "Extra High" },
|
|
2429
|
+
{ id: "high", label: "High" },
|
|
2430
|
+
{ id: "medium", label: "Medium" },
|
|
2431
|
+
{ id: "low", label: "Low" }
|
|
2432
|
+
],
|
|
2433
|
+
"cursor-agent": []
|
|
2434
|
+
};
|
|
2435
|
+
var AGENT_DEFAULT_MODEL = {
|
|
2436
|
+
"claude-code": "opus",
|
|
2437
|
+
codex: "gpt-5.4",
|
|
2438
|
+
"cursor-agent": ""
|
|
2439
|
+
};
|
|
2440
|
+
var AGENT_DEFAULT_EFFORT = {
|
|
2441
|
+
"claude-code": "high",
|
|
2442
|
+
codex: "xhigh",
|
|
2443
|
+
"cursor-agent": ""
|
|
2444
|
+
};
|
|
1878
2445
|
function defaultAgentModelFetchCommand(agent2) {
|
|
1879
|
-
if (agent2 === "cursor")
|
|
2446
|
+
if (agent2 === "cursor-agent")
|
|
1880
2447
|
return "cursor-agent models";
|
|
1881
2448
|
return void 0;
|
|
1882
2449
|
}
|
|
1883
2450
|
function defaultAgentModelOutputFormat(agent2) {
|
|
1884
|
-
if (agent2 === "cursor")
|
|
2451
|
+
if (agent2 === "cursor-agent")
|
|
1885
2452
|
return "lines";
|
|
1886
2453
|
return void 0;
|
|
1887
2454
|
}
|
|
@@ -2093,12 +2660,12 @@ models.command("default <agent>").description("Get or set default model / effort
|
|
|
2093
2660
|
});
|
|
2094
2661
|
|
|
2095
2662
|
// src/commands/agent-run.ts
|
|
2096
|
-
import
|
|
2097
|
-
import
|
|
2663
|
+
import fs18 from "fs";
|
|
2664
|
+
import path17 from "path";
|
|
2098
2665
|
import { Command as Command11 } from "commander";
|
|
2099
2666
|
import chalk11 from "chalk";
|
|
2100
2667
|
import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
|
|
2101
|
-
import
|
|
2668
|
+
import yaml7 from "js-yaml";
|
|
2102
2669
|
var agentRun = new Command11("agent-run").description("Inspect and control agent runs");
|
|
2103
2670
|
agentRun.command("list").description("List active agent runs").option("--json", "Output JSON").option("--task <id>", "Filter by task id").action(async (opts) => {
|
|
2104
2671
|
try {
|
|
@@ -2222,7 +2789,7 @@ profile.command("get <ref>").description("Show one runtime profile by slug or ob
|
|
|
2222
2789
|
console.log(formatProfile(result.runtime));
|
|
2223
2790
|
console.log();
|
|
2224
2791
|
console.log(chalk11.dim("--- exec ---"));
|
|
2225
|
-
console.log(
|
|
2792
|
+
console.log(yaml7.dump(result.runtime.exec, { lineWidth: 100 }));
|
|
2226
2793
|
} catch (err) {
|
|
2227
2794
|
const apiErr = err;
|
|
2228
2795
|
if (apiErr.status === 404) failProfile(`not found: ${ref}`);
|
|
@@ -2232,7 +2799,7 @@ profile.command("get <ref>").description("Show one runtime profile by slug or ob
|
|
|
2232
2799
|
profile.command("create").description("Create a runtime profile from a YAML spec file").requiredOption("--from-file <file>", "YAML spec file with kind, slug, exec").option("--scope <scope>", "Storage scope: user (default) or project", "user").option("--project-root <path>", "Project root (required for --scope project)").action(async (opts) => {
|
|
2233
2800
|
let parsed;
|
|
2234
2801
|
try {
|
|
2235
|
-
parsed =
|
|
2802
|
+
parsed = yaml7.load(fs18.readFileSync(path17.resolve(opts.fromFile), "utf-8"));
|
|
2236
2803
|
} catch (err) {
|
|
2237
2804
|
failProfile(`cannot read ${opts.fromFile}: ${err.message}`);
|
|
2238
2805
|
}
|
|
@@ -2248,14 +2815,14 @@ profile.command("edit <ref>").description("Open the runtime profile YAML in $EDI
|
|
|
2248
2815
|
try {
|
|
2249
2816
|
const result = await api.get(`/api/runtime-profiles/${encodeURIComponent(ref)}`);
|
|
2250
2817
|
if (result.runtime.system) failProfile("cannot edit a system runtime profile");
|
|
2251
|
-
const tmp =
|
|
2252
|
-
|
|
2818
|
+
const tmp = path17.join(process.env.TMPDIR || "/tmp", `task0-runtime-${result.runtime.slug}-${Date.now()}.yml`);
|
|
2819
|
+
fs18.writeFileSync(tmp, yaml7.dump(result.runtime, { lineWidth: 100 }), "utf-8");
|
|
2253
2820
|
const editor = process.env.EDITOR || "vi";
|
|
2254
2821
|
const r = spawnSync3(editor, [tmp], { stdio: "inherit" });
|
|
2255
2822
|
if (r.status !== 0) failProfile(`editor exited with status ${r.status}`);
|
|
2256
|
-
const updated =
|
|
2823
|
+
const updated = yaml7.load(fs18.readFileSync(tmp, "utf-8"));
|
|
2257
2824
|
await api.put(`/api/runtime-profiles/${encodeURIComponent(ref)}`, updated);
|
|
2258
|
-
|
|
2825
|
+
fs18.unlinkSync(tmp);
|
|
2259
2826
|
console.log(chalk11.green(`updated ${ref}`));
|
|
2260
2827
|
} catch (err) {
|
|
2261
2828
|
failProfile(err.message);
|
|
@@ -2275,13 +2842,13 @@ agentRun.addCommand(profile);
|
|
|
2275
2842
|
// src/commands/plan.ts
|
|
2276
2843
|
import { Command as Command12 } from "commander";
|
|
2277
2844
|
import chalk12 from "chalk";
|
|
2278
|
-
import
|
|
2279
|
-
import
|
|
2845
|
+
import fs19 from "fs";
|
|
2846
|
+
import path18 from "path";
|
|
2280
2847
|
init_task_state2();
|
|
2281
2848
|
var PLAN_GENERATE_SKILL_NAME = "plan-generate";
|
|
2282
2849
|
function resolveSkillFilePath3(projectRoot, skillName) {
|
|
2283
|
-
const p =
|
|
2284
|
-
return
|
|
2850
|
+
const p = path18.join(projectRoot, ".claude", "skills", skillName, "SKILL.md");
|
|
2851
|
+
return fs19.existsSync(p) ? p : null;
|
|
2285
2852
|
}
|
|
2286
2853
|
var plan = new Command12("plan").description("Generate and refine plans");
|
|
2287
2854
|
plan.command("generate <objectId>").description("Generate plan(s) from an IDEA file \u2014 supports agent fan-out").option("-a, --agents <list>", "Comma-separated agents (claude-code,codex,cursor)", "codex,claude-code").option("--model <id>", "Model id or alias \u2014 only with a single agent; use `task0 models default` for fan-out").option("--effort <level>", "Reasoning effort \u2014 only with a single agent; use `task0 models default` for fan-out").option("--idea <file>", "IDEA file name (default: latest IDEA-NN.md)").option("--additional-prompt <text>", "Extra prompt content").option("--wait", "Wait for completion").option("--force", "Overwrite existing plan files").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
@@ -2413,7 +2980,7 @@ var PLAN_REFINE_SKILL_NAME = "plan-refine";
|
|
|
2413
2980
|
plan.command("refine <objectId>").description("Synthesize plan files + ISSUE files into a refined plan").option("--agent <name>", "Agent to run refine (claude-code|codex)", "claude-code").option("--model <id>", "Model id or alias (see `task0 models refresh`)").option("--effort <level>", "Reasoning effort (e.g. low|medium|high)").option("--wait", "Wait for completion").option("--json", "Output JSON").action(async (objectId, opts) => {
|
|
2414
2981
|
try {
|
|
2415
2982
|
const loc = resolveTaskByObjectId(objectId);
|
|
2416
|
-
const files =
|
|
2983
|
+
const files = fs19.readdirSync(loc.taskDir);
|
|
2417
2984
|
const planFiles = files.filter((f) => /^PLAN-\d+-(codex|claude-code|cursor)\.md$/.test(f));
|
|
2418
2985
|
if (planFiles.length === 0) {
|
|
2419
2986
|
console.error(chalk12.red("No PLAN-NN-<agent>.md files found. Run `task0 plan generate` first."));
|
|
@@ -2455,8 +3022,8 @@ plan.command("refine <objectId>").description("Synthesize plan files + ISSUE fil
|
|
|
2455
3022
|
if (!opts.json) console.log(chalk12.dim(`[refine] ${s.status}${s.phase ? " " + s.phase : ""}`));
|
|
2456
3023
|
}
|
|
2457
3024
|
});
|
|
2458
|
-
const refinedPath =
|
|
2459
|
-
const wrote =
|
|
3025
|
+
const refinedPath = path18.join(loc.taskDir, refinedFile);
|
|
3026
|
+
const wrote = fs19.existsSync(refinedPath);
|
|
2460
3027
|
if (final.status === "done" && wrote) {
|
|
2461
3028
|
await updateWorkflow(loc.taskYml, {
|
|
2462
3029
|
phase: "refined",
|
|
@@ -2608,8 +3175,8 @@ import { Command as Command14 } from "commander";
|
|
|
2608
3175
|
import chalk14 from "chalk";
|
|
2609
3176
|
|
|
2610
3177
|
// src/lib/project.ts
|
|
2611
|
-
import
|
|
2612
|
-
import
|
|
3178
|
+
import path19 from "path";
|
|
3179
|
+
import fs20 from "fs";
|
|
2613
3180
|
function resolveProjectName(opts) {
|
|
2614
3181
|
if (opts.project && opts.project.length > 0) return opts.project;
|
|
2615
3182
|
const config = loadConfig();
|
|
@@ -2619,15 +3186,15 @@ function resolveProjectName(opts) {
|
|
|
2619
3186
|
"Cannot resolve project: no registered projects. Use `task0 source add <path>` first, or pass --project <name>."
|
|
2620
3187
|
);
|
|
2621
3188
|
}
|
|
2622
|
-
const cwd =
|
|
3189
|
+
const cwd = fs20.realpathSync(process.cwd());
|
|
2623
3190
|
for (const source2 of projects) {
|
|
2624
3191
|
let sourceAbs;
|
|
2625
3192
|
try {
|
|
2626
|
-
sourceAbs =
|
|
3193
|
+
sourceAbs = fs20.realpathSync(path19.resolve(source2.path));
|
|
2627
3194
|
} catch {
|
|
2628
|
-
sourceAbs =
|
|
3195
|
+
sourceAbs = path19.resolve(source2.path);
|
|
2629
3196
|
}
|
|
2630
|
-
if (cwd === sourceAbs || cwd.startsWith(sourceAbs +
|
|
3197
|
+
if (cwd === sourceAbs || cwd.startsWith(sourceAbs + path19.sep)) {
|
|
2631
3198
|
return source2.name;
|
|
2632
3199
|
}
|
|
2633
3200
|
}
|
|
@@ -3267,12 +3834,12 @@ import chalk17 from "chalk";
|
|
|
3267
3834
|
|
|
3268
3835
|
// src/core/issue/decision.ts
|
|
3269
3836
|
init_node();
|
|
3270
|
-
import
|
|
3271
|
-
import
|
|
3837
|
+
import fs21 from "fs";
|
|
3838
|
+
import path20 from "path";
|
|
3272
3839
|
init_task_state2();
|
|
3273
3840
|
var DECISION_FILE_RE = /^DECISION-(\d+)-([a-z0-9-]+)\.md$/;
|
|
3274
3841
|
function selectBlockingIssues(taskDir, explicitIssue) {
|
|
3275
|
-
const files =
|
|
3842
|
+
const files = fs21.existsSync(taskDir) ? fs21.readdirSync(taskDir) : [];
|
|
3276
3843
|
const issueFiles = files.filter((f) => /^ISSUE-\d+\.md$/.test(f)).sort();
|
|
3277
3844
|
const all = readOpenQuestions(taskDir, issueFiles);
|
|
3278
3845
|
if (!explicitIssue) return all;
|
|
@@ -3376,12 +3943,12 @@ function parseConsolidatedAnswers(md) {
|
|
|
3376
3943
|
return sections;
|
|
3377
3944
|
}
|
|
3378
3945
|
function rewriteIssueWithDecisions(taskDir, issueFile, consolidatedFile) {
|
|
3379
|
-
const issuePath =
|
|
3380
|
-
const consolidatedPath =
|
|
3381
|
-
if (!
|
|
3382
|
-
if (!
|
|
3383
|
-
const issueMd =
|
|
3384
|
-
const consolidatedMd =
|
|
3946
|
+
const issuePath = path20.join(taskDir, issueFile);
|
|
3947
|
+
const consolidatedPath = path20.join(taskDir, consolidatedFile);
|
|
3948
|
+
if (!fs21.existsSync(issuePath)) throw new Error(`${issueFile} not found in ${taskDir}`);
|
|
3949
|
+
if (!fs21.existsSync(consolidatedPath)) throw new Error(`${consolidatedFile} not found in ${taskDir}`);
|
|
3950
|
+
const issueMd = fs21.readFileSync(issuePath, "utf-8");
|
|
3951
|
+
const consolidatedMd = fs21.readFileSync(consolidatedPath, "utf-8");
|
|
3385
3952
|
const answers = parseConsolidatedAnswers(consolidatedMd);
|
|
3386
3953
|
if (answers.length === 0) {
|
|
3387
3954
|
throw new Error(`${consolidatedFile} has no "## Qn" sections; cannot derive Decisions`);
|
|
@@ -3403,7 +3970,7 @@ _Resolved from [${consolidatedFile}](${consolidatedFile}); see that file for rea
|
|
|
3403
3970
|
throw new Error(`${issueFile} has no "## Open Questions" section to replace`);
|
|
3404
3971
|
}
|
|
3405
3972
|
const next = issueMd.replace(openQRe, decisionsSection.trimEnd() + "\n");
|
|
3406
|
-
|
|
3973
|
+
fs21.writeFileSync(issuePath, next, "utf-8");
|
|
3407
3974
|
return { replaced: answers.length };
|
|
3408
3975
|
}
|
|
3409
3976
|
async function propose(opts) {
|
|
@@ -3431,8 +3998,8 @@ async function propose(opts) {
|
|
|
3431
3998
|
const kicks = [];
|
|
3432
3999
|
for (const agent2 of opts.agents) {
|
|
3433
4000
|
const decisionFile = decisionFileName(issue2, agent2);
|
|
3434
|
-
const decisionPath =
|
|
3435
|
-
if (
|
|
4001
|
+
const decisionPath = path20.join(loc.taskDir, decisionFile);
|
|
4002
|
+
if (fs21.existsSync(decisionPath) && !opts.force) {
|
|
3436
4003
|
if (opts.ifNeeded) {
|
|
3437
4004
|
kicks.push({
|
|
3438
4005
|
issue: issue2.file,
|
|
@@ -3507,7 +4074,7 @@ async function propose(opts) {
|
|
|
3507
4074
|
const final = await waitForAgentRun(k.agentRunId);
|
|
3508
4075
|
if (final.status !== "done") {
|
|
3509
4076
|
k.error = final.error || `runtime ${k.agentRunId} ended with ${final.status}`;
|
|
3510
|
-
} else if (!
|
|
4077
|
+
} else if (!fs21.existsSync(path20.join(loc.taskDir, k.decisionFile))) {
|
|
3511
4078
|
k.error = `runtime completed but ${k.decisionFile} was not written`;
|
|
3512
4079
|
}
|
|
3513
4080
|
} catch (err) {
|
|
@@ -3528,7 +4095,7 @@ async function consolidate(opts) {
|
|
|
3528
4095
|
const kicks = [];
|
|
3529
4096
|
const modelOpts = resolveModelOptions(opts.agent, { model: opts.model, effort: opts.effort }, opts.warn);
|
|
3530
4097
|
for (const issue2 of issues) {
|
|
3531
|
-
const files =
|
|
4098
|
+
const files = fs21.readdirSync(loc.taskDir);
|
|
3532
4099
|
const proposalFiles = files.filter((f) => {
|
|
3533
4100
|
const m = f.match(DECISION_FILE_RE);
|
|
3534
4101
|
return !!m && m[1] === issue2.index && m[2] !== "consolidated";
|
|
@@ -3574,7 +4141,7 @@ async function consolidate(opts) {
|
|
|
3574
4141
|
if (opts.wait) {
|
|
3575
4142
|
try {
|
|
3576
4143
|
const final = await waitForAgentRun(agentRunId);
|
|
3577
|
-
const wrote =
|
|
4144
|
+
const wrote = fs21.existsSync(path20.join(loc.taskDir, consolidatedFile));
|
|
3578
4145
|
if (final.status !== "done") {
|
|
3579
4146
|
kick.error = final.error || `runtime ${agentRunId} ended with ${final.status}`;
|
|
3580
4147
|
} else if (!wrote) {
|
|
@@ -3640,7 +4207,7 @@ async function approve(opts) {
|
|
|
3640
4207
|
return { updated };
|
|
3641
4208
|
}
|
|
3642
4209
|
function buildReferenceFiles(taskDir, issue2) {
|
|
3643
|
-
const names =
|
|
4210
|
+
const names = fs21.readdirSync(taskDir);
|
|
3644
4211
|
const refs = [issue2.file];
|
|
3645
4212
|
if (names.includes("ISSUE.md")) refs.push("ISSUE.md");
|
|
3646
4213
|
const ideaFile = `IDEA-${issue2.index}.md`;
|
|
@@ -3748,12 +4315,12 @@ issue.command("approve <taskId>").description("Apply the consolidated decisions
|
|
|
3748
4315
|
});
|
|
3749
4316
|
|
|
3750
4317
|
// src/commands/agent.ts
|
|
3751
|
-
import
|
|
3752
|
-
import
|
|
4318
|
+
import fs22 from "fs";
|
|
4319
|
+
import path21 from "path";
|
|
3753
4320
|
import { spawnSync as spawnSync5 } from "child_process";
|
|
3754
4321
|
import { Command as Command18 } from "commander";
|
|
3755
4322
|
import chalk18 from "chalk";
|
|
3756
|
-
import
|
|
4323
|
+
import yaml8 from "js-yaml";
|
|
3757
4324
|
function statusBadge(s) {
|
|
3758
4325
|
if (s === "working") return chalk18.cyan("\u25CF");
|
|
3759
4326
|
if (s === "error") return chalk18.red("\u25CF");
|
|
@@ -3772,7 +4339,7 @@ function fail5(message, code = 1) {
|
|
|
3772
4339
|
process.exit(code);
|
|
3773
4340
|
}
|
|
3774
4341
|
function formatAgent(a, withDetails = false) {
|
|
3775
|
-
const tag = a.
|
|
4342
|
+
const tag = a.scope ? chalk18.dim(`(${a.scope})`) : "";
|
|
3776
4343
|
const dot = statusBadge(a.status);
|
|
3777
4344
|
const head = `${dot} ${chalk18.bold(a.slug.padEnd(24))} ${chalk18.dim(a.object_id.padEnd(16))} ${a.kind.padEnd(10)} ${tag}`;
|
|
3778
4345
|
if (!withDetails) return head;
|
|
@@ -3813,10 +4380,10 @@ agent.command("get <ref>").description("Show one agent by object_id or slug").op
|
|
|
3813
4380
|
}
|
|
3814
4381
|
console.log();
|
|
3815
4382
|
console.log(chalk18.dim("--- spec ---"));
|
|
3816
|
-
console.log(
|
|
4383
|
+
console.log(yaml8.dump(result.agent.spec, { lineWidth: 100 }));
|
|
3817
4384
|
if (result.agent.mcp_config) {
|
|
3818
4385
|
console.log(chalk18.dim("--- mcp_config ---"));
|
|
3819
|
-
console.log(
|
|
4386
|
+
console.log(yaml8.dump(result.agent.mcp_config, { lineWidth: 100 }));
|
|
3820
4387
|
}
|
|
3821
4388
|
} catch (err) {
|
|
3822
4389
|
const apiErr = err;
|
|
@@ -3827,8 +4394,8 @@ agent.command("get <ref>").description("Show one agent by object_id or slug").op
|
|
|
3827
4394
|
agent.command("create").description("Create an agent from a YAML spec file").requiredOption("--from-file <file>", "YAML spec file with at least kind, slug, spec").option("--scope <scope>", "Storage scope: user (default) or project", "user").option("--project-root <path>", "Project root (required for --scope project)").action(async (opts) => {
|
|
3828
4395
|
let parsed;
|
|
3829
4396
|
try {
|
|
3830
|
-
const raw =
|
|
3831
|
-
parsed =
|
|
4397
|
+
const raw = fs22.readFileSync(path21.resolve(opts.fromFile), "utf-8");
|
|
4398
|
+
parsed = yaml8.load(raw);
|
|
3832
4399
|
} catch (err) {
|
|
3833
4400
|
fail5(`cannot read ${opts.fromFile}: ${err.message}`);
|
|
3834
4401
|
}
|
|
@@ -3843,18 +4410,17 @@ agent.command("create").description("Create an agent from a YAML spec file").req
|
|
|
3843
4410
|
agent.command("edit <ref>").description("Open the agent YAML in $EDITOR and save changes").action(async (ref) => {
|
|
3844
4411
|
try {
|
|
3845
4412
|
const result = await api.get(`/api/agents/${encodeURIComponent(ref)}`);
|
|
3846
|
-
|
|
3847
|
-
const tmp = path18.join(
|
|
4413
|
+
const tmp = path21.join(
|
|
3848
4414
|
process.env.TMPDIR || "/tmp",
|
|
3849
4415
|
`task0-agent-${result.agent.slug}-${Date.now()}.yml`
|
|
3850
4416
|
);
|
|
3851
|
-
|
|
4417
|
+
fs22.writeFileSync(tmp, yaml8.dump(result.agent, { lineWidth: 100 }), "utf-8");
|
|
3852
4418
|
const editor = process.env.EDITOR || "vi";
|
|
3853
4419
|
const r = spawnSync5(editor, [tmp], { stdio: "inherit" });
|
|
3854
4420
|
if (r.status !== 0) fail5(`editor exited with status ${r.status}`);
|
|
3855
|
-
const updated =
|
|
4421
|
+
const updated = yaml8.load(fs22.readFileSync(tmp, "utf-8"));
|
|
3856
4422
|
await api.put(`/api/agents/${encodeURIComponent(ref)}`, updated);
|
|
3857
|
-
|
|
4423
|
+
fs22.unlinkSync(tmp);
|
|
3858
4424
|
console.log(chalk18.green(`updated ${ref}`));
|
|
3859
4425
|
} catch (err) {
|
|
3860
4426
|
fail5(err.message);
|
|
@@ -3925,7 +4491,7 @@ var mcp = new Command18("mcp").description("Manage per-agent MCP config (mcp_con
|
|
|
3925
4491
|
mcp.command("set <ref>").description("Set mcp_config from a JSON file (replaces existing config)").requiredOption("--from-file <file>", "JSON file containing the mcp_config object").action(async (ref, opts) => {
|
|
3926
4492
|
let parsed;
|
|
3927
4493
|
try {
|
|
3928
|
-
parsed = JSON.parse(
|
|
4494
|
+
parsed = JSON.parse(fs22.readFileSync(path21.resolve(opts.fromFile), "utf-8"));
|
|
3929
4495
|
} catch (err) {
|
|
3930
4496
|
fail5(`cannot parse ${opts.fromFile}: ${err.message}`);
|
|
3931
4497
|
}
|
|
@@ -3968,78 +4534,77 @@ async function streamOutput(ref, agentRunId) {
|
|
|
3968
4534
|
}
|
|
3969
4535
|
|
|
3970
4536
|
// src/commands/daemon.ts
|
|
3971
|
-
import
|
|
4537
|
+
import os6 from "os";
|
|
4538
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
3972
4539
|
import { Command as Command19 } from "commander";
|
|
3973
4540
|
import chalk19 from "chalk";
|
|
3974
4541
|
import WebSocket from "ws";
|
|
3975
4542
|
|
|
3976
|
-
// src/core/
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
var
|
|
3981
|
-
var
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
this.name = "AdminTokenUnavailableError";
|
|
3991
|
-
}
|
|
3992
|
-
};
|
|
3993
|
-
function readAdminToken() {
|
|
3994
|
-
if (cached) return cached;
|
|
3995
|
-
const fromEnv = process.env.TASK0_ADMIN_TOKEN?.trim();
|
|
3996
|
-
if (fromEnv) {
|
|
3997
|
-
cached = fromEnv;
|
|
3998
|
-
return cached;
|
|
3999
|
-
}
|
|
4000
|
-
if (fs20.existsSync(TOKEN_FILE)) {
|
|
4001
|
-
const v = fs20.readFileSync(TOKEN_FILE, "utf-8").trim();
|
|
4002
|
-
if (v) {
|
|
4003
|
-
cached = v;
|
|
4004
|
-
return cached;
|
|
4543
|
+
// src/core/daemon-agent-run-sink.ts
|
|
4544
|
+
var BUFFER_CAP = 5e3;
|
|
4545
|
+
var bound = null;
|
|
4546
|
+
var seqByRun = /* @__PURE__ */ new Map();
|
|
4547
|
+
var buffer = [];
|
|
4548
|
+
var logHistoryByRun = /* @__PURE__ */ new Map();
|
|
4549
|
+
function bindAgentRunFrameSink(sink) {
|
|
4550
|
+
bound = sink;
|
|
4551
|
+
if (!sink) return;
|
|
4552
|
+
while (buffer.length > 0) {
|
|
4553
|
+
const frame = buffer.shift();
|
|
4554
|
+
try {
|
|
4555
|
+
sink.send(frame);
|
|
4556
|
+
} catch {
|
|
4005
4557
|
}
|
|
4006
4558
|
}
|
|
4007
|
-
throw new AdminTokenUnavailableError();
|
|
4008
|
-
}
|
|
4009
|
-
function adminAuthHeader() {
|
|
4010
|
-
return { authorization: `Bearer ${readAdminToken()}` };
|
|
4011
|
-
}
|
|
4012
|
-
|
|
4013
|
-
// src/core/daemon-config.ts
|
|
4014
|
-
import fs21 from "fs";
|
|
4015
|
-
import os5 from "os";
|
|
4016
|
-
import path20 from "path";
|
|
4017
|
-
var CONFIG_DIR3 = path20.join(os5.homedir(), ".config", "task0");
|
|
4018
|
-
var CONFIG_FILE2 = path20.join(CONFIG_DIR3, "daemon.json");
|
|
4019
|
-
function daemonConfigPath() {
|
|
4020
|
-
return CONFIG_FILE2;
|
|
4021
4559
|
}
|
|
4022
|
-
function
|
|
4023
|
-
if (
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4560
|
+
function deliverOrBuffer(frame) {
|
|
4561
|
+
if (bound) {
|
|
4562
|
+
try {
|
|
4563
|
+
bound.send(frame);
|
|
4564
|
+
return;
|
|
4565
|
+
} catch {
|
|
4566
|
+
}
|
|
4029
4567
|
}
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
fs21.mkdirSync(CONFIG_DIR3, { recursive: true });
|
|
4033
|
-
fs21.writeFileSync(CONFIG_FILE2, JSON.stringify(identity, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
4034
|
-
try {
|
|
4035
|
-
fs21.chmodSync(CONFIG_FILE2, 384);
|
|
4036
|
-
} catch {
|
|
4568
|
+
if (buffer.length >= BUFFER_CAP) {
|
|
4569
|
+
buffer.shift();
|
|
4037
4570
|
}
|
|
4571
|
+
buffer.push(frame);
|
|
4572
|
+
}
|
|
4573
|
+
function pruneRunHistory(runId) {
|
|
4574
|
+
logHistoryByRun.delete(runId);
|
|
4575
|
+
seqByRun.delete(runId);
|
|
4038
4576
|
}
|
|
4039
|
-
function
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4577
|
+
function emitAgentRunStatus(runId, status, opts = {}) {
|
|
4578
|
+
deliverOrBuffer({
|
|
4579
|
+
type: "agent_run_status",
|
|
4580
|
+
run_id: runId,
|
|
4581
|
+
status,
|
|
4582
|
+
phase: opts.phase ?? null,
|
|
4583
|
+
exit_code: opts.exitCode ?? null,
|
|
4584
|
+
error: opts.error ?? null,
|
|
4585
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
4586
|
+
});
|
|
4587
|
+
if (status === "completed" || status === "failed" || status === "killed") {
|
|
4588
|
+
pruneRunHistory(runId);
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
function replayAfterRanges(ranges) {
|
|
4592
|
+
if (!bound) return 0;
|
|
4593
|
+
let sent = 0;
|
|
4594
|
+
for (const { run_id, after_seq } of ranges) {
|
|
4595
|
+
const ring = logHistoryByRun.get(run_id);
|
|
4596
|
+
if (!ring) continue;
|
|
4597
|
+
for (const frame of ring) {
|
|
4598
|
+
if (frame.seq <= after_seq) continue;
|
|
4599
|
+
try {
|
|
4600
|
+
bound.send(frame);
|
|
4601
|
+
sent += 1;
|
|
4602
|
+
} catch {
|
|
4603
|
+
return sent;
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
return sent;
|
|
4043
4608
|
}
|
|
4044
4609
|
|
|
4045
4610
|
// src/core/register-auth.ts
|
|
@@ -4049,7 +4614,7 @@ var RegisterAuthUnavailableError = class extends Error {
|
|
|
4049
4614
|
`No registration credential available.
|
|
4050
4615
|
\u2022 Pass --token <apit_...> with an API token created in the dashboard, or
|
|
4051
4616
|
\u2022 Set TASK0_API_TOKEN in the environment, or
|
|
4052
|
-
\u2022 Fall back to the server's admin token (TASK0_ADMIN_TOKEN or ~/.
|
|
4617
|
+
\u2022 Fall back to the server's admin token (TASK0_ADMIN_TOKEN or ~/.task0/admin.token).`
|
|
4053
4618
|
);
|
|
4054
4619
|
this.name = "RegisterAuthUnavailableError";
|
|
4055
4620
|
}
|
|
@@ -4075,8 +4640,191 @@ function pickRegisterAuth(flagToken) {
|
|
|
4075
4640
|
|
|
4076
4641
|
// src/core/daemon-rpc-handlers.ts
|
|
4077
4642
|
init_node();
|
|
4078
|
-
import
|
|
4079
|
-
import
|
|
4643
|
+
import fs25 from "fs";
|
|
4644
|
+
import path24 from "path";
|
|
4645
|
+
|
|
4646
|
+
// src/core/daemon-agent-run-runner.ts
|
|
4647
|
+
import { execFileSync } from "child_process";
|
|
4648
|
+
import fs24 from "fs";
|
|
4649
|
+
import path23 from "path";
|
|
4650
|
+
|
|
4651
|
+
// src/core/daemon-agent-run-dir.ts
|
|
4652
|
+
init_node();
|
|
4653
|
+
import fs23 from "fs";
|
|
4654
|
+
import path22 from "path";
|
|
4655
|
+
function agentRunRoot() {
|
|
4656
|
+
return process.env.TASK0_DAEMON_AGENT_RUN_DIR || path22.join(task0Home(), "agent-run");
|
|
4657
|
+
}
|
|
4658
|
+
function agentRunDir(runId) {
|
|
4659
|
+
return path22.join(agentRunRoot(), runId);
|
|
4660
|
+
}
|
|
4661
|
+
function agentRunStatusPath(runId) {
|
|
4662
|
+
return path22.join(agentRunDir(runId), "status.json");
|
|
4663
|
+
}
|
|
4664
|
+
function ensureAgentRunDir(runId) {
|
|
4665
|
+
const dir = agentRunDir(runId);
|
|
4666
|
+
fs23.mkdirSync(dir, { recursive: true });
|
|
4667
|
+
return dir;
|
|
4668
|
+
}
|
|
4669
|
+
function removeAgentRunDir(runId) {
|
|
4670
|
+
const dir = agentRunDir(runId);
|
|
4671
|
+
if (!fs23.existsSync(dir)) return;
|
|
4672
|
+
fs23.rmSync(dir, { recursive: true, force: true });
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4675
|
+
// src/core/daemon-agent-run-runner.ts
|
|
4676
|
+
var active = /* @__PURE__ */ new Map();
|
|
4677
|
+
var STATUS_POLL_MS = 500;
|
|
4678
|
+
var SESSION_PROBE_MS = 3e3;
|
|
4679
|
+
function getTmuxBin() {
|
|
4680
|
+
const fromEnv = process.env.TASK0_TMUX_BIN;
|
|
4681
|
+
if (fromEnv) return fromEnv;
|
|
4682
|
+
return "tmux";
|
|
4683
|
+
}
|
|
4684
|
+
function checkTmuxAvailable() {
|
|
4685
|
+
try {
|
|
4686
|
+
execFileSync(getTmuxBin(), ["-V"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
4687
|
+
return { ok: true };
|
|
4688
|
+
} catch (err) {
|
|
4689
|
+
return { ok: false, reason: `tmux not available: ${err instanceof Error ? err.message : String(err)}` };
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
function isSessionAlive2(sessionName) {
|
|
4693
|
+
try {
|
|
4694
|
+
execFileSync(getTmuxBin(), ["has-session", "-t", sessionName], {
|
|
4695
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4696
|
+
});
|
|
4697
|
+
return true;
|
|
4698
|
+
} catch {
|
|
4699
|
+
return false;
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
function killSession2(sessionName) {
|
|
4703
|
+
try {
|
|
4704
|
+
execFileSync(getTmuxBin(), ["kill-session", "-t", sessionName], {
|
|
4705
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4706
|
+
});
|
|
4707
|
+
return true;
|
|
4708
|
+
} catch {
|
|
4709
|
+
return false;
|
|
4710
|
+
}
|
|
4711
|
+
}
|
|
4712
|
+
function readStatusFile(runId) {
|
|
4713
|
+
const filePath = agentRunStatusPath(runId);
|
|
4714
|
+
if (!fs24.existsSync(filePath)) return null;
|
|
4715
|
+
try {
|
|
4716
|
+
const raw = fs24.readFileSync(filePath, "utf-8");
|
|
4717
|
+
const parsed = JSON.parse(raw);
|
|
4718
|
+
return { raw, parsed };
|
|
4719
|
+
} catch {
|
|
4720
|
+
return null;
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
function deriveStatus(parsed) {
|
|
4724
|
+
const raw = typeof parsed.status === "string" ? parsed.status : "starting";
|
|
4725
|
+
let status = "starting";
|
|
4726
|
+
if (raw === "running") status = "running";
|
|
4727
|
+
else if (raw === "done" || raw === "completed") status = "completed";
|
|
4728
|
+
else if (raw === "error" || raw === "failed") status = "failed";
|
|
4729
|
+
const phase = typeof parsed.phase === "string" ? parsed.phase : null;
|
|
4730
|
+
const error2 = typeof parsed.error === "string" && parsed.error ? parsed.error : null;
|
|
4731
|
+
return { status, phase, error: error2 };
|
|
4732
|
+
}
|
|
4733
|
+
function launchAgentRun(params) {
|
|
4734
|
+
const tmuxCheck = checkTmuxAvailable();
|
|
4735
|
+
if (!tmuxCheck.ok) {
|
|
4736
|
+
throw Object.assign(new Error(tmuxCheck.reason ?? "tmux unavailable"), { code: "tmux_unavailable" });
|
|
4737
|
+
}
|
|
4738
|
+
if (active.has(params.runId)) {
|
|
4739
|
+
throw Object.assign(new Error(`agent run already active: ${params.runId}`), { code: "already_running" });
|
|
4740
|
+
}
|
|
4741
|
+
if (!fs24.existsSync(params.workspace)) {
|
|
4742
|
+
throw Object.assign(new Error(`workspace not found: ${params.workspace}`), { code: "workspace_missing" });
|
|
4743
|
+
}
|
|
4744
|
+
const runDir = ensureAgentRunDir(params.runId);
|
|
4745
|
+
const scriptPath = path23.join(runDir, "script.sh");
|
|
4746
|
+
fs24.writeFileSync(scriptPath, params.scriptContent, { mode: 493 });
|
|
4747
|
+
if (params.promptContent !== void 0) {
|
|
4748
|
+
fs24.writeFileSync(path23.join(runDir, "prompt.txt"), params.promptContent, "utf-8");
|
|
4749
|
+
}
|
|
4750
|
+
for (const [name, content] of Object.entries(params.auxFiles ?? {})) {
|
|
4751
|
+
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
4752
|
+
throw Object.assign(new Error(`invalid aux file name: ${name}`), { code: "invalid_params" });
|
|
4753
|
+
}
|
|
4754
|
+
fs24.writeFileSync(path23.join(runDir, name), content, "utf-8");
|
|
4755
|
+
}
|
|
4756
|
+
fs24.writeFileSync(
|
|
4757
|
+
agentRunStatusPath(params.runId),
|
|
4758
|
+
JSON.stringify({ status: "starting", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
|
|
4759
|
+
"utf-8"
|
|
4760
|
+
);
|
|
4761
|
+
try {
|
|
4762
|
+
execFileSync(
|
|
4763
|
+
getTmuxBin(),
|
|
4764
|
+
["new-session", "-d", "-s", params.sessionName, "-c", params.workspace, `bash ${JSON.stringify(scriptPath)}`],
|
|
4765
|
+
{ encoding: "utf-8", timeout: 5e3, stdio: ["ignore", "pipe", "pipe"] }
|
|
4766
|
+
);
|
|
4767
|
+
} catch (err) {
|
|
4768
|
+
removeAgentRunDir(params.runId);
|
|
4769
|
+
throw Object.assign(
|
|
4770
|
+
new Error(`failed to start tmux session: ${err instanceof Error ? err.message : String(err)}`),
|
|
4771
|
+
{ code: "spawn_failed" }
|
|
4772
|
+
);
|
|
4773
|
+
}
|
|
4774
|
+
emitAgentRunStatus(params.runId, "starting", { phase: null });
|
|
4775
|
+
const entry = {
|
|
4776
|
+
runId: params.runId,
|
|
4777
|
+
sessionName: params.sessionName,
|
|
4778
|
+
lastStatusJson: null,
|
|
4779
|
+
finished: false,
|
|
4780
|
+
pollInterval: setInterval(() => pollOne(params.runId), STATUS_POLL_MS)
|
|
4781
|
+
};
|
|
4782
|
+
active.set(params.runId, entry);
|
|
4783
|
+
const sessionProbe = setInterval(() => {
|
|
4784
|
+
const cur = active.get(params.runId);
|
|
4785
|
+
if (!cur || cur.finished) {
|
|
4786
|
+
clearInterval(sessionProbe);
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4789
|
+
if (!isSessionAlive2(cur.sessionName)) {
|
|
4790
|
+
finishRun(params.runId, "killed", { error: "tmux session disappeared" });
|
|
4791
|
+
clearInterval(sessionProbe);
|
|
4792
|
+
}
|
|
4793
|
+
}, SESSION_PROBE_MS);
|
|
4794
|
+
return { agentRunDir: runDir, sessionName: params.sessionName };
|
|
4795
|
+
}
|
|
4796
|
+
function pollOne(runId) {
|
|
4797
|
+
const entry = active.get(runId);
|
|
4798
|
+
if (!entry || entry.finished) return;
|
|
4799
|
+
const current = readStatusFile(runId);
|
|
4800
|
+
if (!current) return;
|
|
4801
|
+
if (current.raw === entry.lastStatusJson) return;
|
|
4802
|
+
entry.lastStatusJson = current.raw;
|
|
4803
|
+
const { status, phase, error: error2 } = deriveStatus(current.parsed);
|
|
4804
|
+
emitAgentRunStatus(runId, status, { phase, error: error2 });
|
|
4805
|
+
if (status === "completed" || status === "failed") {
|
|
4806
|
+
finishRun(runId, status, { phase, error: error2 });
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
function finishRun(runId, status, opts = {}) {
|
|
4810
|
+
const entry = active.get(runId);
|
|
4811
|
+
if (!entry || entry.finished) return;
|
|
4812
|
+
entry.finished = true;
|
|
4813
|
+
clearInterval(entry.pollInterval);
|
|
4814
|
+
emitAgentRunStatus(runId, status, opts);
|
|
4815
|
+
active.delete(runId);
|
|
4816
|
+
}
|
|
4817
|
+
function cancelAgentRun(runId) {
|
|
4818
|
+
const entry = active.get(runId);
|
|
4819
|
+
if (!entry) {
|
|
4820
|
+
return { ok: false, sessionName: null };
|
|
4821
|
+
}
|
|
4822
|
+
killSession2(entry.sessionName);
|
|
4823
|
+
finishRun(runId, "killed", { error: "cancelled by hub" });
|
|
4824
|
+
return { ok: true, sessionName: entry.sessionName };
|
|
4825
|
+
}
|
|
4826
|
+
|
|
4827
|
+
// src/core/daemon-rpc-handlers.ts
|
|
4080
4828
|
var MAX_FILE_BYTES = 1 * 1024 * 1024;
|
|
4081
4829
|
function ensureString(value, name) {
|
|
4082
4830
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -4084,13 +4832,61 @@ function ensureString(value, name) {
|
|
|
4084
4832
|
}
|
|
4085
4833
|
return value;
|
|
4086
4834
|
}
|
|
4835
|
+
function ensureSafeTaskId(value) {
|
|
4836
|
+
const id = ensureString(value, "taskId");
|
|
4837
|
+
if (id.includes("/") || id.includes("\\") || id.includes("\0") || id === "." || id === ".." || path24.isAbsolute(id)) {
|
|
4838
|
+
throw Object.assign(new Error(`invalid taskId: ${id}`), { code: "invalid_params" });
|
|
4839
|
+
}
|
|
4840
|
+
return id;
|
|
4841
|
+
}
|
|
4842
|
+
function assertContained(rootAbs, resolvedAbs) {
|
|
4843
|
+
const root = path24.resolve(rootAbs);
|
|
4844
|
+
const target = path24.resolve(resolvedAbs);
|
|
4845
|
+
if (target !== root && !target.startsWith(root + path24.sep)) {
|
|
4846
|
+
throw Object.assign(new Error("path escapes containment root"), { code: "invalid_params" });
|
|
4847
|
+
}
|
|
4848
|
+
}
|
|
4849
|
+
function optionalString(value) {
|
|
4850
|
+
if (value === void 0 || value === null) return void 0;
|
|
4851
|
+
if (typeof value !== "string") {
|
|
4852
|
+
throw Object.assign(new Error("expected string"), { code: "invalid_params" });
|
|
4853
|
+
}
|
|
4854
|
+
return value;
|
|
4855
|
+
}
|
|
4856
|
+
function optionalBoolean(value) {
|
|
4857
|
+
if (value === void 0 || value === null) return void 0;
|
|
4858
|
+
if (typeof value !== "boolean") {
|
|
4859
|
+
throw Object.assign(new Error("expected boolean"), { code: "invalid_params" });
|
|
4860
|
+
}
|
|
4861
|
+
return value;
|
|
4862
|
+
}
|
|
4863
|
+
function listProjects() {
|
|
4864
|
+
return loadConfig().sources.filter((source2) => source2.type === "project");
|
|
4865
|
+
}
|
|
4866
|
+
function applyRepair(r) {
|
|
4867
|
+
const raw = readYaml(r.taskYml);
|
|
4868
|
+
if (!raw) return;
|
|
4869
|
+
if (r.reason === "missing_object_id") raw.object_id = generateObjectId("task");
|
|
4870
|
+
if (r.reason === "id_mismatch") raw.id = r.dirName;
|
|
4871
|
+
writeYaml(r.taskYml, raw);
|
|
4872
|
+
}
|
|
4873
|
+
function scanProjectWithRepair(projectPath, sourceName) {
|
|
4874
|
+
let result = scanProject(projectPath, sourceName);
|
|
4875
|
+
if (result.repairs.length > 0) {
|
|
4876
|
+
for (const r of result.repairs) applyRepair(r);
|
|
4877
|
+
result = scanProject(projectPath, sourceName);
|
|
4878
|
+
}
|
|
4879
|
+
return result;
|
|
4880
|
+
}
|
|
4087
4881
|
var rpcHandlers = {
|
|
4088
4882
|
// Scan a local project for its task manifest. Returns the same shape the
|
|
4089
|
-
// in-process
|
|
4883
|
+
// hub's in-process scanProject() used to return — with auto-repair of
|
|
4884
|
+
// missing object_id / id mismatches applied in place on the daemon's FS.
|
|
4090
4885
|
async scan_project(params) {
|
|
4091
4886
|
const rootPath = ensureString(params.rootPath, "rootPath");
|
|
4092
|
-
const name = typeof params.name === "string" && params.name ? params.name :
|
|
4093
|
-
|
|
4887
|
+
const name = typeof params.name === "string" && params.name ? params.name : path24.basename(rootPath);
|
|
4888
|
+
const result = scanProjectWithRepair(rootPath, name);
|
|
4889
|
+
return { tasks: result.tasks, errors: result.errors };
|
|
4094
4890
|
},
|
|
4095
4891
|
// Read a file from disk on the daemon's host. The dashboard uses this to
|
|
4096
4892
|
// peek into archived task content, runtime logs, etc. Cap is 1 MiB to
|
|
@@ -4099,7 +4895,7 @@ var rpcHandlers = {
|
|
|
4099
4895
|
const filePath = ensureString(params.path, "path");
|
|
4100
4896
|
let stat;
|
|
4101
4897
|
try {
|
|
4102
|
-
stat =
|
|
4898
|
+
stat = fs25.statSync(filePath);
|
|
4103
4899
|
} catch {
|
|
4104
4900
|
throw Object.assign(new Error("file not found"), { code: "not_found" });
|
|
4105
4901
|
}
|
|
@@ -4109,23 +4905,261 @@ var rpcHandlers = {
|
|
|
4109
4905
|
if (stat.size > MAX_FILE_BYTES) {
|
|
4110
4906
|
throw Object.assign(new Error(`file too large (${stat.size} bytes > ${MAX_FILE_BYTES})`), { code: "too_large" });
|
|
4111
4907
|
}
|
|
4112
|
-
const content =
|
|
4908
|
+
const content = fs25.readFileSync(filePath, "utf-8");
|
|
4113
4909
|
return { content, size: stat.size, modifiedAt: stat.mtime.toISOString() };
|
|
4910
|
+
},
|
|
4911
|
+
// ---------------------------------------------------------------------
|
|
4912
|
+
// Project source CRUD. The daemon owns its host's project list; the hub
|
|
4913
|
+
// routes /api/daemons/:id/projects through these handlers instead of
|
|
4914
|
+
// writing config.yml directly. After every mutation we re-push manifest
|
|
4915
|
+
// so the hub's daemon_projects table catches up without waiting for a
|
|
4916
|
+
// restart.
|
|
4917
|
+
// ---------------------------------------------------------------------
|
|
4918
|
+
async project_list() {
|
|
4919
|
+
return { projects: listProjects() };
|
|
4920
|
+
},
|
|
4921
|
+
async project_add(params, ctx) {
|
|
4922
|
+
const rawPath = ensureString(params.path, "path");
|
|
4923
|
+
const absPath = path24.resolve(rawPath);
|
|
4924
|
+
if (!fs25.existsSync(absPath)) {
|
|
4925
|
+
throw Object.assign(new Error(`path does not exist: ${absPath}`), { code: "not_found" });
|
|
4926
|
+
}
|
|
4927
|
+
const stat = fs25.statSync(absPath);
|
|
4928
|
+
if (!stat.isDirectory()) {
|
|
4929
|
+
throw Object.assign(new Error(`path is not a directory: ${absPath}`), { code: "invalid_target" });
|
|
4930
|
+
}
|
|
4931
|
+
const name = optionalString(params.name)?.trim() || path24.basename(absPath);
|
|
4932
|
+
const result = scanProjectWithRepair(absPath, name);
|
|
4933
|
+
addSource({ name, type: "project", path: absPath, enabled: true });
|
|
4934
|
+
ctx.notifyManifestChanged();
|
|
4935
|
+
return {
|
|
4936
|
+
project: { name, type: "project", path: absPath, enabled: true },
|
|
4937
|
+
taskCount: result.tasks.length
|
|
4938
|
+
};
|
|
4939
|
+
},
|
|
4940
|
+
async project_remove(params, ctx) {
|
|
4941
|
+
const name = ensureString(params.name, "name");
|
|
4942
|
+
const ok = removeSource(name);
|
|
4943
|
+
if (!ok) {
|
|
4944
|
+
throw Object.assign(new Error(`project not found: ${name}`), { code: "not_found" });
|
|
4945
|
+
}
|
|
4946
|
+
ctx.notifyManifestChanged();
|
|
4947
|
+
return { ok: true };
|
|
4948
|
+
},
|
|
4949
|
+
async project_set_enabled(params, ctx) {
|
|
4950
|
+
const name = ensureString(params.name, "name");
|
|
4951
|
+
const enabled = optionalBoolean(params.enabled);
|
|
4952
|
+
if (enabled === void 0) {
|
|
4953
|
+
throw Object.assign(new Error("enabled is required (boolean)"), { code: "invalid_params" });
|
|
4954
|
+
}
|
|
4955
|
+
const config = loadConfig();
|
|
4956
|
+
const source2 = config.sources.find((s) => s.name === name && s.type === "project");
|
|
4957
|
+
if (!source2) {
|
|
4958
|
+
throw Object.assign(new Error(`project not found: ${name}`), { code: "not_found" });
|
|
4959
|
+
}
|
|
4960
|
+
source2.enabled = enabled;
|
|
4961
|
+
saveConfig(config);
|
|
4962
|
+
ctx.notifyManifestChanged();
|
|
4963
|
+
return { project: source2 };
|
|
4964
|
+
},
|
|
4965
|
+
// ---------------------------------------------------------------------
|
|
4966
|
+
// Task content R/W. These are the deep-pull operations from Q5(c): the
|
|
4967
|
+
// hub asks the daemon to read/write/create/delete a task's yaml + sibling
|
|
4968
|
+
// files on the daemon's FS. After every mutation we re-push manifest so
|
|
4969
|
+
// hub's daemon_tasks index reflects the new state without a restart.
|
|
4970
|
+
// ---------------------------------------------------------------------
|
|
4971
|
+
// Read a task's task0.yml (parsed) plus an optional list of sibling files
|
|
4972
|
+
// in the same task directory. Path is resolved from (projectName, taskId).
|
|
4973
|
+
async task_read(params) {
|
|
4974
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
4975
|
+
const taskId = ensureSafeTaskId(params.taskId);
|
|
4976
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
4977
|
+
if (!project2) {
|
|
4978
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
4979
|
+
}
|
|
4980
|
+
const projectAbs = path24.resolve(project2.path);
|
|
4981
|
+
const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
|
|
4982
|
+
if (!projectConfig) {
|
|
4983
|
+
throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
|
|
4984
|
+
}
|
|
4985
|
+
const tasksRoot = path24.resolve(projectAbs, projectConfig.tasks_dir);
|
|
4986
|
+
const taskDir = path24.resolve(tasksRoot, taskId);
|
|
4987
|
+
assertContained(tasksRoot, taskDir);
|
|
4988
|
+
if (!fs25.existsSync(taskDir)) {
|
|
4989
|
+
throw Object.assign(new Error(`task not found: ${taskId}`), { code: "not_found" });
|
|
4990
|
+
}
|
|
4991
|
+
const taskYml = path24.join(taskDir, "task0.yml");
|
|
4992
|
+
const yaml10 = readYaml(taskYml);
|
|
4993
|
+
if (!yaml10) {
|
|
4994
|
+
throw Object.assign(new Error(`task yaml missing or unreadable: ${taskYml}`), { code: "not_found" });
|
|
4995
|
+
}
|
|
4996
|
+
const files = fs25.readdirSync(taskDir).filter((name) => name !== "task0.yml");
|
|
4997
|
+
return { task_dir: taskDir, yaml: yaml10, files };
|
|
4998
|
+
},
|
|
4999
|
+
// Create a task directory + write task0.yml + write any additional named
|
|
5000
|
+
// files. Hub decides the taskId and yaml content (slug/object_id are
|
|
5001
|
+
// generated server-side); daemon just commits to disk.
|
|
5002
|
+
async task_create(params, ctx) {
|
|
5003
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
5004
|
+
const taskId = ensureSafeTaskId(params.taskId);
|
|
5005
|
+
const yaml10 = params.yaml;
|
|
5006
|
+
if (!yaml10 || typeof yaml10 !== "object") {
|
|
5007
|
+
throw Object.assign(new Error("yaml (object) is required"), { code: "invalid_params" });
|
|
5008
|
+
}
|
|
5009
|
+
const files = params.files ?? {};
|
|
5010
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
5011
|
+
if (!project2) {
|
|
5012
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
5013
|
+
}
|
|
5014
|
+
const projectAbs = path24.resolve(project2.path);
|
|
5015
|
+
const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
|
|
5016
|
+
if (!projectConfig) {
|
|
5017
|
+
throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
|
|
5018
|
+
}
|
|
5019
|
+
const tasksRoot = path24.resolve(projectAbs, projectConfig.tasks_dir);
|
|
5020
|
+
const taskDir = path24.resolve(tasksRoot, taskId);
|
|
5021
|
+
assertContained(tasksRoot, taskDir);
|
|
5022
|
+
if (fs25.existsSync(taskDir)) {
|
|
5023
|
+
throw Object.assign(new Error(`task already exists: ${taskId}`), { code: "already_exists" });
|
|
5024
|
+
}
|
|
5025
|
+
fs25.mkdirSync(taskDir, { recursive: true });
|
|
5026
|
+
writeYaml(path24.join(taskDir, "task0.yml"), yaml10);
|
|
5027
|
+
for (const [name, content] of Object.entries(files)) {
|
|
5028
|
+
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
5029
|
+
throw Object.assign(new Error(`invalid file name: ${name}`), { code: "invalid_params" });
|
|
5030
|
+
}
|
|
5031
|
+
fs25.writeFileSync(path24.join(taskDir, name), content, "utf-8");
|
|
5032
|
+
}
|
|
5033
|
+
ctx.notifyManifestChanged();
|
|
5034
|
+
return { task_dir: taskDir };
|
|
5035
|
+
},
|
|
5036
|
+
// Patch a task's task0.yml in place. The hub computes the desired yaml
|
|
5037
|
+
// (typically by loading + applying its own metadata patch), then sends
|
|
5038
|
+
// the full replacement object back. Atomic at the file level — no
|
|
5039
|
+
// partial writes.
|
|
5040
|
+
async task_write_yaml(params, ctx) {
|
|
5041
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
5042
|
+
const taskId = ensureSafeTaskId(params.taskId);
|
|
5043
|
+
const yaml10 = params.yaml;
|
|
5044
|
+
if (!yaml10 || typeof yaml10 !== "object") {
|
|
5045
|
+
throw Object.assign(new Error("yaml (object) is required"), { code: "invalid_params" });
|
|
5046
|
+
}
|
|
5047
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
5048
|
+
if (!project2) {
|
|
5049
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
5050
|
+
}
|
|
5051
|
+
const projectAbs = path24.resolve(project2.path);
|
|
5052
|
+
const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
|
|
5053
|
+
if (!projectConfig) {
|
|
5054
|
+
throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
|
|
5055
|
+
}
|
|
5056
|
+
const tasksRoot = path24.resolve(projectAbs, projectConfig.tasks_dir);
|
|
5057
|
+
const taskDir = path24.resolve(tasksRoot, taskId);
|
|
5058
|
+
assertContained(tasksRoot, taskDir);
|
|
5059
|
+
const taskYml = path24.join(taskDir, "task0.yml");
|
|
5060
|
+
if (!fs25.existsSync(taskYml)) {
|
|
5061
|
+
throw Object.assign(new Error(`task yaml not found: ${taskYml}`), { code: "not_found" });
|
|
5062
|
+
}
|
|
5063
|
+
writeYaml(taskYml, yaml10);
|
|
5064
|
+
ctx.notifyManifestChanged();
|
|
5065
|
+
return { task_dir: taskDir };
|
|
5066
|
+
},
|
|
5067
|
+
// Delete an entire task directory (use sparingly; mostly for archive
|
|
5068
|
+
// workflows). Returns the deleted path so the hub can emit an event.
|
|
5069
|
+
async task_delete(params, ctx) {
|
|
5070
|
+
const projectName = ensureString(params.projectName, "projectName");
|
|
5071
|
+
const taskId = ensureSafeTaskId(params.taskId);
|
|
5072
|
+
const project2 = listProjects().find((p) => p.name === projectName);
|
|
5073
|
+
if (!project2) {
|
|
5074
|
+
throw Object.assign(new Error(`project not found: ${projectName}`), { code: "not_found" });
|
|
5075
|
+
}
|
|
5076
|
+
const projectAbs = path24.resolve(project2.path);
|
|
5077
|
+
const projectConfig = readYaml(path24.join(projectAbs, "task0.yml"));
|
|
5078
|
+
if (!projectConfig) {
|
|
5079
|
+
throw Object.assign(new Error(`invalid project: missing task0.yml at ${projectAbs}`), { code: "invalid_project" });
|
|
5080
|
+
}
|
|
5081
|
+
const tasksRoot = path24.resolve(projectAbs, projectConfig.tasks_dir);
|
|
5082
|
+
const taskDir = path24.resolve(tasksRoot, taskId);
|
|
5083
|
+
assertContained(tasksRoot, taskDir);
|
|
5084
|
+
if (!fs25.existsSync(taskDir)) {
|
|
5085
|
+
throw Object.assign(new Error(`task not found: ${taskId}`), { code: "not_found" });
|
|
5086
|
+
}
|
|
5087
|
+
fs25.rmSync(taskDir, { recursive: true, force: true });
|
|
5088
|
+
ctx.notifyManifestChanged();
|
|
5089
|
+
return { task_dir: taskDir, deleted: true };
|
|
5090
|
+
},
|
|
5091
|
+
// ---------------------------------------------------------------------
|
|
5092
|
+
// Agent run lifecycle (P3.A scaffolding — handlers reject until P3.B
|
|
5093
|
+
// moves the actual spawn / tmux / supervise logic from hub's
|
|
5094
|
+
// agent-run-launcher.ts into the daemon).
|
|
5095
|
+
//
|
|
5096
|
+
// Wire shape (subject to refinement in P3.B):
|
|
5097
|
+
// agent_run_launch → {projectName, runId, runType, agentSlug,
|
|
5098
|
+
// workspace, prompt, ...} → {tmuxSession}
|
|
5099
|
+
// Daemon then streams agent_run_log /
|
|
5100
|
+
// agent_run_status frames over WS.
|
|
5101
|
+
// agent_run_cancel → {runId} → {ok: true}
|
|
5102
|
+
// ---------------------------------------------------------------------
|
|
5103
|
+
// Skill discovery on the daemon's host. Mirrors hub-side
|
|
5104
|
+
// resolveAgentSkill() — given a workspace + agent + filePath, returns
|
|
5105
|
+
// the matching AgentSkill record or null. P3.C uses this so the hub
|
|
5106
|
+
// can dispatch agent runs to remote daemons without itself needing FS
|
|
5107
|
+
// access to the workspace.
|
|
5108
|
+
async agent_skill_resolve(params) {
|
|
5109
|
+
const workspace = ensureString(params.workspace, "workspace");
|
|
5110
|
+
const agent2 = ensureString(params.agent, "agent");
|
|
5111
|
+
const filePath = ensureString(params.filePath, "filePath");
|
|
5112
|
+
if (agent2 !== "claude_code" && agent2 !== "codex" && agent2 !== "cursor") {
|
|
5113
|
+
throw Object.assign(new Error(`unknown skill agent: ${agent2}`), { code: "invalid_params" });
|
|
5114
|
+
}
|
|
5115
|
+
const projectSkills = getProjectAgentSkills(workspace);
|
|
5116
|
+
const globalSkills = getGlobalAgentSkills();
|
|
5117
|
+
const all = [...projectSkills, ...globalSkills];
|
|
5118
|
+
const resolved = path24.resolve(filePath);
|
|
5119
|
+
const skill = all.find(
|
|
5120
|
+
(s) => s.agent === agent2 && path24.resolve(s.filePath) === resolved
|
|
5121
|
+
) ?? null;
|
|
5122
|
+
return { skill };
|
|
5123
|
+
},
|
|
5124
|
+
async agent_run_launch(params) {
|
|
5125
|
+
const runId = ensureString(params.runId, "runId");
|
|
5126
|
+
const sessionName = ensureString(params.sessionName, "sessionName");
|
|
5127
|
+
const workspace = ensureString(params.workspace, "workspace");
|
|
5128
|
+
const scriptContent = ensureString(params.scriptContent, "scriptContent");
|
|
5129
|
+
const auxFiles = params.auxFiles && typeof params.auxFiles === "object" ? params.auxFiles : {};
|
|
5130
|
+
const promptContent = typeof params.promptContent === "string" ? params.promptContent : void 0;
|
|
5131
|
+
const { agentRunDir: agentRunDir2, sessionName: actualSessionName } = launchAgentRun({
|
|
5132
|
+
runId,
|
|
5133
|
+
sessionName,
|
|
5134
|
+
workspace,
|
|
5135
|
+
scriptContent,
|
|
5136
|
+
auxFiles,
|
|
5137
|
+
promptContent
|
|
5138
|
+
});
|
|
5139
|
+
return { agent_run_dir: agentRunDir2, tmux_session: actualSessionName };
|
|
5140
|
+
},
|
|
5141
|
+
async agent_run_cancel(params) {
|
|
5142
|
+
const runId = ensureString(params.runId, "runId");
|
|
5143
|
+
const result = cancelAgentRun(runId);
|
|
5144
|
+
if (!result.ok) {
|
|
5145
|
+
throw Object.assign(new Error(`no active agent run: ${runId}`), { code: "not_found" });
|
|
5146
|
+
}
|
|
5147
|
+
return { ok: true, tmux_session: result.sessionName };
|
|
4114
5148
|
}
|
|
4115
5149
|
};
|
|
4116
5150
|
|
|
4117
5151
|
// src/core/daemon-service/launchd.ts
|
|
4118
5152
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
4119
|
-
import
|
|
4120
|
-
import
|
|
5153
|
+
import fs27 from "fs";
|
|
5154
|
+
import path26 from "path";
|
|
4121
5155
|
|
|
4122
5156
|
// src/core/daemon-service/binary.ts
|
|
4123
|
-
import
|
|
5157
|
+
import fs26 from "fs";
|
|
4124
5158
|
import { fileURLToPath } from "url";
|
|
4125
5159
|
function resolveTask0Invocation() {
|
|
4126
5160
|
const node = process.execPath;
|
|
4127
5161
|
const argv1 = process.argv[1] ?? fileURLToPath(import.meta.url);
|
|
4128
|
-
const main2 =
|
|
5162
|
+
const main2 = fs26.realpathSync(argv1);
|
|
4129
5163
|
if (!isInstalledBuild(main2) && process.env.TASK0_ALLOW_DEV_SERVICE !== "1") {
|
|
4130
5164
|
throw new Error(
|
|
4131
5165
|
`Refusing to install autostart service pointing at ${main2}.
|
|
@@ -4141,31 +5175,40 @@ function isInstalledBuild(p) {
|
|
|
4141
5175
|
}
|
|
4142
5176
|
|
|
4143
5177
|
// src/core/daemon-service/paths.ts
|
|
4144
|
-
|
|
4145
|
-
import
|
|
5178
|
+
init_node();
|
|
5179
|
+
import os5 from "os";
|
|
5180
|
+
import path25 from "path";
|
|
4146
5181
|
|
|
4147
5182
|
// src/core/daemon-service/types.ts
|
|
4148
|
-
|
|
5183
|
+
import crypto2 from "crypto";
|
|
5184
|
+
var BASE_SERVICE_LABEL = "cc.cy0.task0";
|
|
5185
|
+
function serviceLabel() {
|
|
5186
|
+
const home = process.env.TASK0_HOME;
|
|
5187
|
+
if (!home || home.length === 0) return BASE_SERVICE_LABEL;
|
|
5188
|
+
const hash = crypto2.createHash("sha256").update(home).digest("hex").slice(0, 8);
|
|
5189
|
+
return `${BASE_SERVICE_LABEL}.${hash}`;
|
|
5190
|
+
}
|
|
4149
5191
|
|
|
4150
5192
|
// src/core/daemon-service/paths.ts
|
|
4151
5193
|
function unitPath(scope) {
|
|
4152
5194
|
const platform = process.platform;
|
|
5195
|
+
const label = serviceLabel();
|
|
4153
5196
|
if (platform === "darwin") {
|
|
4154
|
-
return scope === "user" ?
|
|
5197
|
+
return scope === "user" ? path25.join(os5.homedir(), "Library", "LaunchAgents", `${label}.plist`) : path25.join("/", "Library", "LaunchDaemons", `${label}.plist`);
|
|
4155
5198
|
}
|
|
4156
5199
|
if (platform === "linux") {
|
|
4157
|
-
return scope === "user" ?
|
|
5200
|
+
return scope === "user" ? path25.join(os5.homedir(), ".config", "systemd", "user", `${label}.service`) : path25.join("/", "etc", "systemd", "system", `${label}.service`);
|
|
4158
5201
|
}
|
|
4159
5202
|
throw new Error(`Unsupported platform for service install: ${platform}`);
|
|
4160
5203
|
}
|
|
4161
5204
|
function logDir() {
|
|
4162
|
-
return
|
|
5205
|
+
return path25.join(task0Home(), "logs");
|
|
4163
5206
|
}
|
|
4164
5207
|
function logPaths() {
|
|
4165
5208
|
const dir = logDir();
|
|
4166
5209
|
return {
|
|
4167
|
-
out:
|
|
4168
|
-
err:
|
|
5210
|
+
out: path25.join(dir, "daemon.out.log"),
|
|
5211
|
+
err: path25.join(dir, "daemon.err.log")
|
|
4169
5212
|
};
|
|
4170
5213
|
}
|
|
4171
5214
|
|
|
@@ -4173,14 +5216,29 @@ function logPaths() {
|
|
|
4173
5216
|
function escapeXml(s) {
|
|
4174
5217
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
4175
5218
|
}
|
|
5219
|
+
function collectTask0Env() {
|
|
5220
|
+
const out = {};
|
|
5221
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
5222
|
+
if (k.startsWith("TASK0_") && typeof v === "string") out[k] = v;
|
|
5223
|
+
}
|
|
5224
|
+
return out;
|
|
5225
|
+
}
|
|
4176
5226
|
function renderPlist(opts) {
|
|
4177
5227
|
const programArgs = [opts.node, opts.main, ...opts.args].map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
5228
|
+
const envLines = [
|
|
5229
|
+
` <key>PATH</key>`,
|
|
5230
|
+
` <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>`,
|
|
5231
|
+
...Object.entries(opts.task0Env ?? {}).flatMap(([k, v]) => [
|
|
5232
|
+
` <key>${escapeXml(k)}</key>`,
|
|
5233
|
+
` <string>${escapeXml(v)}</string>`
|
|
5234
|
+
])
|
|
5235
|
+
].join("\n");
|
|
4178
5236
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
4179
5237
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4180
5238
|
<plist version="1.0">
|
|
4181
5239
|
<dict>
|
|
4182
5240
|
<key>Label</key>
|
|
4183
|
-
<string>${
|
|
5241
|
+
<string>${serviceLabel()}</string>
|
|
4184
5242
|
<key>ProgramArguments</key>
|
|
4185
5243
|
<array>
|
|
4186
5244
|
${programArgs}
|
|
@@ -4202,8 +5260,7 @@ ${programArgs}
|
|
|
4202
5260
|
<string>${escapeXml(opts.home)}</string>
|
|
4203
5261
|
<key>EnvironmentVariables</key>
|
|
4204
5262
|
<dict>
|
|
4205
|
-
|
|
4206
|
-
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
5263
|
+
${envLines}
|
|
4207
5264
|
</dict>
|
|
4208
5265
|
</dict>
|
|
4209
5266
|
</plist>
|
|
@@ -4215,7 +5272,7 @@ function domainTarget(scope) {
|
|
|
4215
5272
|
return `gui/${uid}`;
|
|
4216
5273
|
}
|
|
4217
5274
|
function serviceTarget(scope) {
|
|
4218
|
-
return `${domainTarget(scope)}/${
|
|
5275
|
+
return `${domainTarget(scope)}/${serviceLabel()}`;
|
|
4219
5276
|
}
|
|
4220
5277
|
function run2(cmd, args) {
|
|
4221
5278
|
const res = spawnSync6(cmd, args, { encoding: "utf-8" });
|
|
@@ -4226,17 +5283,18 @@ function createLaunchdManager(scope) {
|
|
|
4226
5283
|
const logs = logPaths();
|
|
4227
5284
|
async function install() {
|
|
4228
5285
|
const inv = resolveTask0Invocation();
|
|
4229
|
-
|
|
4230
|
-
|
|
5286
|
+
fs27.mkdirSync(logDir(), { recursive: true });
|
|
5287
|
+
fs27.mkdirSync(path26.dirname(file), { recursive: true });
|
|
4231
5288
|
const body = renderPlist({
|
|
4232
5289
|
node: inv.node,
|
|
4233
5290
|
main: inv.main,
|
|
4234
5291
|
args: inv.args,
|
|
4235
5292
|
home: process.env.HOME ?? "/",
|
|
4236
5293
|
out: logs.out,
|
|
4237
|
-
err: logs.err
|
|
5294
|
+
err: logs.err,
|
|
5295
|
+
task0Env: collectTask0Env()
|
|
4238
5296
|
});
|
|
4239
|
-
|
|
5297
|
+
fs27.writeFileSync(file, body, { mode: 420 });
|
|
4240
5298
|
const bootstrap = run2("launchctl", ["bootstrap", domainTarget(scope), file]);
|
|
4241
5299
|
if (bootstrap.code !== 0) {
|
|
4242
5300
|
const already = /already loaded|service already bootstrapped/i.test(bootstrap.stderr);
|
|
@@ -4262,9 +5320,9 @@ function createLaunchdManager(scope) {
|
|
|
4262
5320
|
}
|
|
4263
5321
|
async function uninstall() {
|
|
4264
5322
|
run2("launchctl", ["bootout", serviceTarget(scope)]);
|
|
4265
|
-
if (
|
|
5323
|
+
if (fs27.existsSync(file)) {
|
|
4266
5324
|
run2("launchctl", ["unload", file]);
|
|
4267
|
-
|
|
5325
|
+
fs27.unlinkSync(file);
|
|
4268
5326
|
}
|
|
4269
5327
|
}
|
|
4270
5328
|
async function start() {
|
|
@@ -4283,7 +5341,7 @@ function createLaunchdManager(scope) {
|
|
|
4283
5341
|
}
|
|
4284
5342
|
}
|
|
4285
5343
|
async function status() {
|
|
4286
|
-
if (!
|
|
5344
|
+
if (!fs27.existsSync(file)) return "absent";
|
|
4287
5345
|
const printed = run2("launchctl", ["print", serviceTarget(scope)]);
|
|
4288
5346
|
if (printed.code !== 0) return "installed";
|
|
4289
5347
|
const out = printed.stdout;
|
|
@@ -4306,15 +5364,26 @@ function createLaunchdManager(scope) {
|
|
|
4306
5364
|
|
|
4307
5365
|
// src/core/daemon-service/systemd.ts
|
|
4308
5366
|
import { spawnSync as spawnSync7 } from "child_process";
|
|
4309
|
-
import
|
|
4310
|
-
import
|
|
5367
|
+
import fs28 from "fs";
|
|
5368
|
+
import path27 from "path";
|
|
4311
5369
|
function shellEscape(s) {
|
|
4312
5370
|
if (!/[\s"\\$]/.test(s)) return s;
|
|
4313
5371
|
return `"${s.replace(/[\\"]/g, (m) => `\\${m}`)}"`;
|
|
4314
5372
|
}
|
|
5373
|
+
function collectTask0Env2() {
|
|
5374
|
+
const out = {};
|
|
5375
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
5376
|
+
if (k.startsWith("TASK0_") && typeof v === "string") out[k] = v;
|
|
5377
|
+
}
|
|
5378
|
+
return out;
|
|
5379
|
+
}
|
|
4315
5380
|
function renderUnit(opts) {
|
|
4316
5381
|
const execStart = [opts.node, opts.main, ...opts.args].map(shellEscape).join(" ");
|
|
4317
5382
|
const wantedBy = opts.scope === "user" ? "default.target" : "multi-user.target";
|
|
5383
|
+
const envLines = ["Environment=NODE_ENV=production"];
|
|
5384
|
+
for (const [k, v] of Object.entries(opts.task0Env ?? {})) {
|
|
5385
|
+
envLines.push(`Environment=${k}=${shellEscape(v)}`);
|
|
5386
|
+
}
|
|
4318
5387
|
return `[Unit]
|
|
4319
5388
|
Description=task0 daemon \u2014 central-server bridge
|
|
4320
5389
|
After=network-online.target
|
|
@@ -4327,7 +5396,7 @@ Restart=on-failure
|
|
|
4327
5396
|
RestartSec=5s
|
|
4328
5397
|
StandardOutput=append:${opts.out}
|
|
4329
5398
|
StandardError=append:${opts.err}
|
|
4330
|
-
|
|
5399
|
+
${envLines.join("\n")}
|
|
4331
5400
|
|
|
4332
5401
|
[Install]
|
|
4333
5402
|
WantedBy=${wantedBy}
|
|
@@ -4343,20 +5412,21 @@ function run3(cmd, args) {
|
|
|
4343
5412
|
function createSystemdManager(scope) {
|
|
4344
5413
|
const file = unitPath(scope);
|
|
4345
5414
|
const logs = logPaths();
|
|
4346
|
-
const unitName = `${
|
|
5415
|
+
const unitName = `${serviceLabel()}.service`;
|
|
4347
5416
|
async function install() {
|
|
4348
5417
|
const inv = resolveTask0Invocation();
|
|
4349
|
-
|
|
4350
|
-
|
|
5418
|
+
fs28.mkdirSync(logDir(), { recursive: true });
|
|
5419
|
+
fs28.mkdirSync(path27.dirname(file), { recursive: true });
|
|
4351
5420
|
const body = renderUnit({
|
|
4352
5421
|
node: inv.node,
|
|
4353
5422
|
main: inv.main,
|
|
4354
5423
|
args: inv.args,
|
|
4355
5424
|
out: logs.out,
|
|
4356
5425
|
err: logs.err,
|
|
4357
|
-
scope
|
|
5426
|
+
scope,
|
|
5427
|
+
task0Env: collectTask0Env2()
|
|
4358
5428
|
});
|
|
4359
|
-
|
|
5429
|
+
fs28.writeFileSync(file, body, { mode: 420 });
|
|
4360
5430
|
const reload = run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
|
|
4361
5431
|
if (reload.code !== 0) {
|
|
4362
5432
|
throw new Error(`systemctl daemon-reload failed: ${reload.stderr}`);
|
|
@@ -4365,8 +5435,8 @@ function createSystemdManager(scope) {
|
|
|
4365
5435
|
}
|
|
4366
5436
|
async function uninstall() {
|
|
4367
5437
|
run3("systemctl", [...scopeFlag(scope), "disable", "--now", unitName]);
|
|
4368
|
-
if (
|
|
4369
|
-
|
|
5438
|
+
if (fs28.existsSync(file)) {
|
|
5439
|
+
fs28.unlinkSync(file);
|
|
4370
5440
|
}
|
|
4371
5441
|
run3("systemctl", [...scopeFlag(scope), "daemon-reload"]);
|
|
4372
5442
|
}
|
|
@@ -4383,7 +5453,7 @@ function createSystemdManager(scope) {
|
|
|
4383
5453
|
}
|
|
4384
5454
|
}
|
|
4385
5455
|
async function status() {
|
|
4386
|
-
if (!
|
|
5456
|
+
if (!fs28.existsSync(file)) return "absent";
|
|
4387
5457
|
const res = run3("systemctl", [...scopeFlag(scope), "is-active", unitName]);
|
|
4388
5458
|
const out = res.stdout.trim();
|
|
4389
5459
|
if (out === "active") return "running";
|
|
@@ -4416,8 +5486,100 @@ function getServiceManager(scope) {
|
|
|
4416
5486
|
);
|
|
4417
5487
|
}
|
|
4418
5488
|
|
|
5489
|
+
// src/core/cli-version.ts
|
|
5490
|
+
import { readFileSync } from "fs";
|
|
5491
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5492
|
+
import path28 from "path";
|
|
5493
|
+
var cached2 = null;
|
|
5494
|
+
function readCliVersion() {
|
|
5495
|
+
if (cached2 !== null) return cached2;
|
|
5496
|
+
const here = path28.dirname(fileURLToPath2(import.meta.url));
|
|
5497
|
+
const candidates = [
|
|
5498
|
+
path28.resolve(here, "..", "package.json"),
|
|
5499
|
+
path28.resolve(here, "..", "..", "package.json")
|
|
5500
|
+
];
|
|
5501
|
+
for (const candidate of candidates) {
|
|
5502
|
+
try {
|
|
5503
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
5504
|
+
if (pkg.name === "@task0/cli" && pkg.version) {
|
|
5505
|
+
cached2 = pkg.version;
|
|
5506
|
+
return cached2;
|
|
5507
|
+
}
|
|
5508
|
+
} catch {
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
cached2 = "unknown";
|
|
5512
|
+
return cached2;
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5515
|
+
// src/core/scaffold-default-agents.ts
|
|
5516
|
+
init_node();
|
|
5517
|
+
import fs29 from "fs";
|
|
5518
|
+
import path29 from "path";
|
|
5519
|
+
import yaml9 from "js-yaml";
|
|
5520
|
+
function userAgentsDir() {
|
|
5521
|
+
return path29.join(task0Home(), "agents");
|
|
5522
|
+
}
|
|
5523
|
+
var NAMES = {
|
|
5524
|
+
"claude-code": "Claude Code",
|
|
5525
|
+
codex: "Codex",
|
|
5526
|
+
"cursor-agent": "Cursor"
|
|
5527
|
+
};
|
|
5528
|
+
var DESCRIPTIONS = {
|
|
5529
|
+
"claude-code": "Anthropic Claude Code CLI, launched inside a tmux session against the task workspace.",
|
|
5530
|
+
codex: "OpenAI Codex CLI, launched with `codex exec` against the task workspace.",
|
|
5531
|
+
"cursor-agent": "Cursor agent CLI (`cursor-agent`), launched against the task workspace."
|
|
5532
|
+
};
|
|
5533
|
+
var STABLE_OBJECT_IDS = {
|
|
5534
|
+
"claude-code": "agt_sysCC",
|
|
5535
|
+
codex: "agt_sysCX",
|
|
5536
|
+
"cursor-agent": "agt_sysCR"
|
|
5537
|
+
};
|
|
5538
|
+
function buildDefaultAgentYaml(provider) {
|
|
5539
|
+
const fetchCommand = defaultAgentModelFetchCommand(provider);
|
|
5540
|
+
const fetchFormat = defaultAgentModelOutputFormat(provider);
|
|
5541
|
+
return {
|
|
5542
|
+
object_id: STABLE_OBJECT_IDS[provider],
|
|
5543
|
+
slug: provider,
|
|
5544
|
+
name: NAMES[provider],
|
|
5545
|
+
description: DESCRIPTIONS[provider],
|
|
5546
|
+
kind: "coding",
|
|
5547
|
+
spec: {
|
|
5548
|
+
agent_provider: provider,
|
|
5549
|
+
model: AGENT_DEFAULT_MODEL[provider],
|
|
5550
|
+
effort: AGENT_DEFAULT_EFFORT[provider],
|
|
5551
|
+
available_models: AGENT_MODEL_DEFAULTS[provider].map((m) => ({ id: m.id, label: m.label })),
|
|
5552
|
+
available_efforts: AGENT_EFFORT_DEFAULTS[provider].map((m) => ({ id: m.id, label: m.label })),
|
|
5553
|
+
...fetchCommand ? { model_fetch_command: fetchCommand } : {},
|
|
5554
|
+
...fetchFormat ? { model_fetch_format: fetchFormat } : {}
|
|
5555
|
+
}
|
|
5556
|
+
};
|
|
5557
|
+
}
|
|
5558
|
+
function isDirectoryEmpty(dir) {
|
|
5559
|
+
if (!fs29.existsSync(dir)) return true;
|
|
5560
|
+
try {
|
|
5561
|
+
return fs29.readdirSync(dir).length === 0;
|
|
5562
|
+
} catch {
|
|
5563
|
+
return true;
|
|
5564
|
+
}
|
|
5565
|
+
}
|
|
5566
|
+
function scaffoldDefaultAgentsIfEmpty() {
|
|
5567
|
+
const dir = userAgentsDir();
|
|
5568
|
+
if (!isDirectoryEmpty(dir)) {
|
|
5569
|
+
return { scaffolded: false, written: [] };
|
|
5570
|
+
}
|
|
5571
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
5572
|
+
const written = [];
|
|
5573
|
+
for (const provider of AGENT_PROVIDERS) {
|
|
5574
|
+
const file = path29.join(dir, `${provider}.yml`);
|
|
5575
|
+
const body = yaml9.dump(buildDefaultAgentYaml(provider), { lineWidth: 100 });
|
|
5576
|
+
fs29.writeFileSync(file, body, "utf-8");
|
|
5577
|
+
written.push(file);
|
|
5578
|
+
}
|
|
5579
|
+
return { scaffolded: true, written };
|
|
5580
|
+
}
|
|
5581
|
+
|
|
4419
5582
|
// src/commands/daemon.ts
|
|
4420
|
-
var DAEMON_VERSION = "0.1.0";
|
|
4421
5583
|
async function dispatchRpc(ws, id, method, params) {
|
|
4422
5584
|
const handler = rpcHandlers[method];
|
|
4423
5585
|
if (!handler) {
|
|
@@ -4425,7 +5587,8 @@ async function dispatchRpc(ws, id, method, params) {
|
|
|
4425
5587
|
return;
|
|
4426
5588
|
}
|
|
4427
5589
|
try {
|
|
4428
|
-
const
|
|
5590
|
+
const ctx = { notifyManifestChanged: () => pushManifest(ws) };
|
|
5591
|
+
const result = await handler(params ?? {}, ctx);
|
|
4429
5592
|
sendRpc(ws, { type: "rpc_response", id, result });
|
|
4430
5593
|
} catch (error2) {
|
|
4431
5594
|
const code = error2 && typeof error2 === "object" && "code" in error2 && typeof error2.code === "string" ? error2.code : "handler_error";
|
|
@@ -4438,6 +5601,79 @@ function sendRpc(ws, payload) {
|
|
|
4438
5601
|
ws.send(JSON.stringify(payload));
|
|
4439
5602
|
}
|
|
4440
5603
|
}
|
|
5604
|
+
function summariseTask(projectName, task2) {
|
|
5605
|
+
const { object_id = null, id, status, title, task_dir, ...extra } = task2;
|
|
5606
|
+
return {
|
|
5607
|
+
project: projectName,
|
|
5608
|
+
object_id: typeof object_id === "string" ? object_id : null,
|
|
5609
|
+
id,
|
|
5610
|
+
status,
|
|
5611
|
+
title,
|
|
5612
|
+
task_dir,
|
|
5613
|
+
extra
|
|
5614
|
+
};
|
|
5615
|
+
}
|
|
5616
|
+
function buildManifest() {
|
|
5617
|
+
const projectSources = loadConfig().sources.filter((source2) => source2.type === "project");
|
|
5618
|
+
const projects = projectSources.map((source2) => ({
|
|
5619
|
+
name: source2.name,
|
|
5620
|
+
path: source2.path,
|
|
5621
|
+
enabled: source2.enabled
|
|
5622
|
+
}));
|
|
5623
|
+
const tasks = [];
|
|
5624
|
+
const scanErrors = {};
|
|
5625
|
+
for (const source2 of projectSources) {
|
|
5626
|
+
if (!source2.enabled) continue;
|
|
5627
|
+
try {
|
|
5628
|
+
const result = scanProjectWithRepair(source2.path, source2.name);
|
|
5629
|
+
for (const task2 of result.tasks) tasks.push(summariseTask(source2.name, task2));
|
|
5630
|
+
if (result.errors.length > 0) scanErrors[source2.name] = result.errors;
|
|
5631
|
+
} catch (err) {
|
|
5632
|
+
scanErrors[source2.name] = [err instanceof Error ? err.message : String(err)];
|
|
5633
|
+
}
|
|
5634
|
+
}
|
|
5635
|
+
return { type: "manifest", projects, tasks, scan_errors: scanErrors };
|
|
5636
|
+
}
|
|
5637
|
+
function pushManifest(ws) {
|
|
5638
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
5639
|
+
const manifest = buildManifest();
|
|
5640
|
+
ws.send(JSON.stringify(manifest));
|
|
5641
|
+
}
|
|
5642
|
+
var AGENT_PROVIDER_BINARIES = {
|
|
5643
|
+
"claude-code": "claude",
|
|
5644
|
+
codex: "codex",
|
|
5645
|
+
"cursor-agent": "cursor-agent"
|
|
5646
|
+
};
|
|
5647
|
+
function detectAgentProvider(provider) {
|
|
5648
|
+
const binary = AGENT_PROVIDER_BINARIES[provider];
|
|
5649
|
+
let resolvedPath;
|
|
5650
|
+
try {
|
|
5651
|
+
resolvedPath = execFileSync2("which", [binary], {
|
|
5652
|
+
encoding: "utf-8",
|
|
5653
|
+
timeout: 2e3,
|
|
5654
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5655
|
+
}).trim();
|
|
5656
|
+
} catch {
|
|
5657
|
+
return null;
|
|
5658
|
+
}
|
|
5659
|
+
if (!resolvedPath) return null;
|
|
5660
|
+
let version = null;
|
|
5661
|
+
try {
|
|
5662
|
+
version = execFileSync2(binary, ["--version"], {
|
|
5663
|
+
encoding: "utf-8",
|
|
5664
|
+
timeout: 3e3,
|
|
5665
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5666
|
+
}).trim();
|
|
5667
|
+
} catch {
|
|
5668
|
+
version = null;
|
|
5669
|
+
}
|
|
5670
|
+
return { agent_provider: provider, path: resolvedPath, version };
|
|
5671
|
+
}
|
|
5672
|
+
function detectInstalledAgentProviders() {
|
|
5673
|
+
return AGENT_PROVIDERS.map(detectAgentProvider).filter(
|
|
5674
|
+
(entry) => entry !== null
|
|
5675
|
+
);
|
|
5676
|
+
}
|
|
4441
5677
|
function fail6(message, code = 1) {
|
|
4442
5678
|
console.error(chalk19.red(message));
|
|
4443
5679
|
process.exit(code);
|
|
@@ -4508,7 +5744,7 @@ daemonCmd.command("register").description("Register this host with a central ser
|
|
|
4508
5744
|
throw error2;
|
|
4509
5745
|
}
|
|
4510
5746
|
const body = {
|
|
4511
|
-
hostname:
|
|
5747
|
+
hostname: os6.hostname(),
|
|
4512
5748
|
platform: process.platform,
|
|
4513
5749
|
name: opts.name
|
|
4514
5750
|
};
|
|
@@ -4600,6 +5836,12 @@ daemonCmd.command("stop").description("Stop the autostart service via launchctl
|
|
|
4600
5836
|
});
|
|
4601
5837
|
daemonCmd.command("run").description("Run the daemon WebSocket loop in foreground (invoked by the service unit; useful for debugging)").action(async () => {
|
|
4602
5838
|
const identity = loadRequiredIdentity();
|
|
5839
|
+
const scaffold = scaffoldDefaultAgentsIfEmpty();
|
|
5840
|
+
if (scaffold.scaffolded) {
|
|
5841
|
+
console.log(
|
|
5842
|
+
chalk19.green(`Scaffolded ${scaffold.written.length} default agent yml(s) under ~/.task0/agents/`)
|
|
5843
|
+
);
|
|
5844
|
+
}
|
|
4603
5845
|
const wsUrl = identity.server_url.replace(/^http/, "ws").replace(/\/$/, "") + "/ws/daemon";
|
|
4604
5846
|
console.log(chalk19.green(`Starting daemon ${identity.daemon_id} \u2192 ${wsUrl}`));
|
|
4605
5847
|
let reconnectDelay = 1e3;
|
|
@@ -4613,18 +5855,34 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
|
|
|
4613
5855
|
ws.on("open", () => {
|
|
4614
5856
|
reconnectDelay = 1e3;
|
|
4615
5857
|
console.log(chalk19.green(`[${(/* @__PURE__ */ new Date()).toISOString()}] connected`));
|
|
5858
|
+
const installedAgentProviders = detectInstalledAgentProviders();
|
|
5859
|
+
if (installedAgentProviders.length > 0) {
|
|
5860
|
+
console.log(
|
|
5861
|
+
chalk19.dim(
|
|
5862
|
+
`detected agent providers: ${installedAgentProviders.map((p) => `${p.agent_provider}${p.version ? ` (${p.version})` : ""}`).join(", ")}`
|
|
5863
|
+
)
|
|
5864
|
+
);
|
|
5865
|
+
} else {
|
|
5866
|
+
console.log(chalk19.yellow("no agent providers detected on PATH (claude / codex / cursor-agent)"));
|
|
5867
|
+
}
|
|
4616
5868
|
const hello = {
|
|
4617
5869
|
type: "hello",
|
|
4618
5870
|
daemon_id: identity.daemon_id,
|
|
4619
|
-
version:
|
|
5871
|
+
version: readCliVersion(),
|
|
4620
5872
|
hostname: identity.hostname,
|
|
4621
|
-
platform: identity.platform
|
|
5873
|
+
platform: identity.platform,
|
|
5874
|
+
installed_agent_providers: installedAgentProviders
|
|
4622
5875
|
};
|
|
4623
5876
|
ws.send(JSON.stringify(hello));
|
|
4624
|
-
const
|
|
4625
|
-
const manifest = { type: "manifest", projects };
|
|
5877
|
+
const manifest = buildManifest();
|
|
4626
5878
|
ws.send(JSON.stringify(manifest));
|
|
4627
|
-
console.log(chalk19.dim(`pushed manifest: ${projects.length} project(s)`));
|
|
5879
|
+
console.log(chalk19.dim(`pushed manifest: ${manifest.projects.length} project(s)`));
|
|
5880
|
+
const sink = {
|
|
5881
|
+
send: (frame) => {
|
|
5882
|
+
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(frame));
|
|
5883
|
+
}
|
|
5884
|
+
};
|
|
5885
|
+
bindAgentRunFrameSink(sink);
|
|
4628
5886
|
});
|
|
4629
5887
|
ws.on("message", (raw) => {
|
|
4630
5888
|
let msg;
|
|
@@ -4642,10 +5900,16 @@ daemonCmd.command("run").description("Run the daemon WebSocket loop in foregroun
|
|
|
4642
5900
|
console.error(chalk19.yellow(`server error: ${msg.message}`));
|
|
4643
5901
|
} else if (msg.type === "rpc_request") {
|
|
4644
5902
|
void dispatchRpc(ws, msg.id, msg.method, msg.params);
|
|
5903
|
+
} else if (msg.type === "agent_run_resume_request") {
|
|
5904
|
+
const sent = replayAfterRanges(msg.ranges);
|
|
5905
|
+
if (sent > 0) {
|
|
5906
|
+
console.log(chalk19.dim(`replayed ${sent} agent-run log frame(s) for hub`));
|
|
5907
|
+
}
|
|
4645
5908
|
}
|
|
4646
5909
|
});
|
|
4647
5910
|
ws.on("close", (code, reason) => {
|
|
4648
5911
|
activeWs = null;
|
|
5912
|
+
bindAgentRunFrameSink(null);
|
|
4649
5913
|
const reasonText = reason.toString("utf-8") || "no reason";
|
|
4650
5914
|
console.log(chalk19.yellow(`[${(/* @__PURE__ */ new Date()).toISOString()}] disconnected (code=${code}, ${reasonText})`));
|
|
4651
5915
|
if (code === 4001) {
|
|
@@ -4709,6 +5973,55 @@ daemonCmd.command("show [daemonId]").description("Show local daemon identity (no
|
|
|
4709
5973
|
const data = await jsonGet(`${base}/api/daemons/${encodeURIComponent(daemonId)}`);
|
|
4710
5974
|
console.log(JSON.stringify(data.daemon, null, 2));
|
|
4711
5975
|
});
|
|
5976
|
+
daemonCmd.command("status").description("Show this host's daemon status (identity, CLI version, service state)").option("--system", "Inspect the system-layer service status (default: user)").option("--json", "Emit a machine-readable JSON report instead of formatted text").action(async (opts) => {
|
|
5977
|
+
const identity = loadRequiredIdentity();
|
|
5978
|
+
const cliVersion = readCliVersion();
|
|
5979
|
+
const scope = opts.system ? "system" : "user";
|
|
5980
|
+
let serviceState = "unsupported";
|
|
5981
|
+
let unitPath2 = null;
|
|
5982
|
+
let serviceError = null;
|
|
5983
|
+
if (isPlatformSupported()) {
|
|
5984
|
+
try {
|
|
5985
|
+
const svc = getServiceManager(scope);
|
|
5986
|
+
serviceState = await svc.status();
|
|
5987
|
+
unitPath2 = svc.unitPath();
|
|
5988
|
+
} catch (error2) {
|
|
5989
|
+
serviceState = "unknown";
|
|
5990
|
+
serviceError = error2 instanceof Error ? error2.message : String(error2);
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
if (opts.json) {
|
|
5994
|
+
console.log(JSON.stringify({
|
|
5995
|
+
daemon_id: identity.daemon_id,
|
|
5996
|
+
name: identity.name,
|
|
5997
|
+
hostname: identity.hostname,
|
|
5998
|
+
platform: identity.platform,
|
|
5999
|
+
server_url: identity.server_url,
|
|
6000
|
+
registered_at: identity.registered_at,
|
|
6001
|
+
cli_version: cliVersion,
|
|
6002
|
+
service: {
|
|
6003
|
+
scope,
|
|
6004
|
+
state: serviceState,
|
|
6005
|
+
unit_path: unitPath2,
|
|
6006
|
+
error: serviceError
|
|
6007
|
+
}
|
|
6008
|
+
}, null, 2));
|
|
6009
|
+
return;
|
|
6010
|
+
}
|
|
6011
|
+
const stateColor = serviceState === "running" ? chalk19.green : serviceState === "stopped" || serviceState === "absent" ? chalk19.yellow : serviceState === "errored" || serviceState === "unknown" ? chalk19.red : chalk19.dim;
|
|
6012
|
+
const label = (s) => chalk19.bold(s.padEnd(11));
|
|
6013
|
+
console.log(`${label("daemon")}${identity.daemon_id} (${identity.name})`);
|
|
6014
|
+
console.log(`${label("host")}${identity.hostname} \xB7 ${identity.platform}`);
|
|
6015
|
+
console.log(`${label("server")}${identity.server_url}`);
|
|
6016
|
+
console.log(`${label("cli")}v${cliVersion}`);
|
|
6017
|
+
console.log(`${label("registered")}${identity.registered_at}`);
|
|
6018
|
+
if (isPlatformSupported()) {
|
|
6019
|
+
console.log(`${label("service")}${stateColor(serviceState)} (${scope})${unitPath2 ? ` \u2192 ${unitPath2}` : ""}`);
|
|
6020
|
+
if (serviceError) console.log(chalk19.dim(` ${serviceError}`));
|
|
6021
|
+
} else {
|
|
6022
|
+
console.log(`${label("service")}${chalk19.dim(`not supported on ${process.platform}`)}`);
|
|
6023
|
+
}
|
|
6024
|
+
});
|
|
4712
6025
|
daemonCmd.command("logout").description("Stop and uninstall the autostart service, then forget the locally stored daemon identity").option("--system", "Target the system-layer service").option("--keep-service", "Leave the installed service unit in place; only clear the identity file").action(async (opts) => {
|
|
4713
6026
|
const scope = opts.system ? "system" : "user";
|
|
4714
6027
|
if (!opts.keepService) {
|
|
@@ -4735,16 +6048,15 @@ daemonCmd.command("logout").description("Stop and uninstall the autostart servic
|
|
|
4735
6048
|
// src/commands/user.ts
|
|
4736
6049
|
import { Command as Command20 } from "commander";
|
|
4737
6050
|
import chalk20 from "chalk";
|
|
4738
|
-
var DEFAULT_BASE2 = (process.env.TASK0_API_URL || "http://127.0.0.1:4318").replace(/\/$/, "");
|
|
4739
6051
|
function serverBase2() {
|
|
4740
|
-
return
|
|
6052
|
+
return (process.env.TASK0_API_URL || "http://127.0.0.1:4318").replace(/\/$/, "");
|
|
4741
6053
|
}
|
|
4742
6054
|
function fail7(message, code = 1) {
|
|
4743
6055
|
console.error(chalk20.red(message));
|
|
4744
6056
|
process.exit(code);
|
|
4745
6057
|
}
|
|
4746
|
-
async function callServer(
|
|
4747
|
-
const url = `${serverBase2()}${
|
|
6058
|
+
async function callServer(path31, init = {}) {
|
|
6059
|
+
const url = `${serverBase2()}${path31}`;
|
|
4748
6060
|
let auth;
|
|
4749
6061
|
try {
|
|
4750
6062
|
auth = adminAuthHeader();
|
|
@@ -5247,6 +6559,160 @@ automation.command("runs <id>").description("List recent runs for an automation"
|
|
|
5247
6559
|
for (const run4 of automation_runs) printRun(run4);
|
|
5248
6560
|
});
|
|
5249
6561
|
|
|
6562
|
+
// src/commands/profile.ts
|
|
6563
|
+
import fs30 from "fs";
|
|
6564
|
+
import os7 from "os";
|
|
6565
|
+
import path30 from "path";
|
|
6566
|
+
import { Command as Command23 } from "commander";
|
|
6567
|
+
import chalk23 from "chalk";
|
|
6568
|
+
var VALID_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
6569
|
+
function fail9(msg) {
|
|
6570
|
+
console.error(chalk23.red(msg));
|
|
6571
|
+
process.exit(1);
|
|
6572
|
+
}
|
|
6573
|
+
function legacyTask0Home() {
|
|
6574
|
+
return path30.join(os7.homedir(), ".task0");
|
|
6575
|
+
}
|
|
6576
|
+
function readDaemonAt(home) {
|
|
6577
|
+
const file = path30.join(home, "daemon.json");
|
|
6578
|
+
if (!fs30.existsSync(file)) return null;
|
|
6579
|
+
try {
|
|
6580
|
+
return JSON.parse(fs30.readFileSync(file, "utf-8"));
|
|
6581
|
+
} catch {
|
|
6582
|
+
return null;
|
|
6583
|
+
}
|
|
6584
|
+
}
|
|
6585
|
+
var profile2 = new Command23("profile").description("Manage named CLI profiles (each isolates TASK0_HOME)");
|
|
6586
|
+
profile2.command("list").description("List configured profiles").action(() => {
|
|
6587
|
+
const profiles = listProfiles();
|
|
6588
|
+
const current = getCurrentProfileName();
|
|
6589
|
+
const names = Object.keys(profiles);
|
|
6590
|
+
if (names.length === 0) {
|
|
6591
|
+
console.log("No profiles configured. Add one with `task0 profile add <name> --task0-home <path>`.");
|
|
6592
|
+
console.log(chalk23.dim(`(currently running in legacy mode \u2014 TASK0_HOME=${process.env.TASK0_HOME ?? legacyTask0Home()})`));
|
|
6593
|
+
return;
|
|
6594
|
+
}
|
|
6595
|
+
for (const name of names) {
|
|
6596
|
+
const entry = profiles[name];
|
|
6597
|
+
const marker = name === current ? chalk23.green("\u25CF ") : " ";
|
|
6598
|
+
const url = entry.api_url ? chalk23.dim(` \u2192 ${entry.api_url}`) : "";
|
|
6599
|
+
console.log(`${marker}${name} ${chalk23.dim(entry.task0_home)}${url}`);
|
|
6600
|
+
}
|
|
6601
|
+
if (!current) {
|
|
6602
|
+
console.log(chalk23.dim("\nNo current profile selected. Use `task0 profile use <name>` to activate one."));
|
|
6603
|
+
}
|
|
6604
|
+
});
|
|
6605
|
+
profile2.command("add <name>").description("Register a new profile").requiredOption("--task0-home <path>", "Directory to hold this profile's daemon/admin state (will be created if missing)").option("--api-url <url>", "API server URL the CLI should call when this profile is active").action((name, opts) => {
|
|
6606
|
+
if (!VALID_NAME.test(name)) {
|
|
6607
|
+
fail9(`Invalid profile name "${name}". Must match ${VALID_NAME}.`);
|
|
6608
|
+
}
|
|
6609
|
+
const existing = getProfile(name);
|
|
6610
|
+
if (existing) {
|
|
6611
|
+
fail9(`Profile "${name}" already exists.`);
|
|
6612
|
+
}
|
|
6613
|
+
const absHome = path30.resolve(opts.task0Home);
|
|
6614
|
+
const profiles = listProfiles();
|
|
6615
|
+
for (const [otherName, entry2] of Object.entries(profiles)) {
|
|
6616
|
+
if (path30.resolve(entry2.task0_home) === absHome) {
|
|
6617
|
+
fail9(`Profile "${otherName}" already uses task0_home "${absHome}". Two profiles cannot share a task0_home (service labels collide).`);
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
const parent = path30.dirname(absHome);
|
|
6621
|
+
if (!fs30.existsSync(parent)) {
|
|
6622
|
+
fail9(`Parent directory does not exist: ${parent}`);
|
|
6623
|
+
}
|
|
6624
|
+
try {
|
|
6625
|
+
fs30.accessSync(parent, fs30.constants.W_OK);
|
|
6626
|
+
} catch {
|
|
6627
|
+
fail9(`Parent directory is not writable: ${parent}`);
|
|
6628
|
+
}
|
|
6629
|
+
const isFirstAdd = Object.keys(profiles).length === 0;
|
|
6630
|
+
if (isFirstAdd && name !== "default") {
|
|
6631
|
+
const legacyHome = legacyTask0Home();
|
|
6632
|
+
const legacy = readDaemonAt(legacyHome);
|
|
6633
|
+
if (legacy) {
|
|
6634
|
+
addProfile("default", {
|
|
6635
|
+
task0_home: legacyHome,
|
|
6636
|
+
...legacy.server_url ? { api_url: legacy.server_url } : {}
|
|
6637
|
+
});
|
|
6638
|
+
console.log(chalk23.dim(`Auto-imported existing ~/.task0 state as profile "default" (use \`task0 profile use default\` to keep using it).`));
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
const entry = {
|
|
6642
|
+
task0_home: absHome,
|
|
6643
|
+
...opts.apiUrl ? { api_url: opts.apiUrl } : {}
|
|
6644
|
+
};
|
|
6645
|
+
addProfile(name, entry);
|
|
6646
|
+
fs30.mkdirSync(absHome, { recursive: true });
|
|
6647
|
+
const adopted = readDaemonAt(absHome);
|
|
6648
|
+
if (adopted) {
|
|
6649
|
+
console.log(chalk23.yellow(`warn: ${absHome} already contains daemon state. Profile "${name}" will adopt it.`));
|
|
6650
|
+
}
|
|
6651
|
+
console.log(chalk23.green(`Added profile "${name}"`));
|
|
6652
|
+
console.log(` task0_home: ${absHome}`);
|
|
6653
|
+
if (entry.api_url) console.log(` api_url: ${entry.api_url}`);
|
|
6654
|
+
console.log(chalk23.dim(`Use \`task0 profile use ${name}\` to activate.`));
|
|
6655
|
+
});
|
|
6656
|
+
profile2.command("remove <name>").description("Remove a profile entry (does not delete the task0_home directory)").option("-f, --force", "Allow removing the current profile (clears current_profile)").action((name, opts) => {
|
|
6657
|
+
const entry = getProfile(name);
|
|
6658
|
+
if (!entry) {
|
|
6659
|
+
fail9(`Profile "${name}" not found.`);
|
|
6660
|
+
}
|
|
6661
|
+
const current = getCurrentProfileName();
|
|
6662
|
+
if (current === name && !opts.force) {
|
|
6663
|
+
fail9(`Profile "${name}" is current. Re-run with --force to remove it (this clears current_profile).`);
|
|
6664
|
+
}
|
|
6665
|
+
removeProfile(name);
|
|
6666
|
+
console.log(chalk23.green(`Removed profile "${name}".`));
|
|
6667
|
+
if (current === name) {
|
|
6668
|
+
console.log(chalk23.dim("current_profile cleared. Use `task0 profile use <name>` to pick another."));
|
|
6669
|
+
}
|
|
6670
|
+
console.log(chalk23.dim(`Note: ${entry.task0_home} was not deleted.`));
|
|
6671
|
+
});
|
|
6672
|
+
profile2.command("use <name>").description("Set the current profile").action((name) => {
|
|
6673
|
+
const entry = getProfile(name);
|
|
6674
|
+
if (!entry) {
|
|
6675
|
+
const names = Object.keys(listProfiles());
|
|
6676
|
+
fail9(`Profile "${name}" not found. Available: ${names.length > 0 ? names.join(", ") : "(none)"}.`);
|
|
6677
|
+
}
|
|
6678
|
+
setCurrentProfile(name);
|
|
6679
|
+
console.log(chalk23.green(`Now using profile "${name}".`));
|
|
6680
|
+
});
|
|
6681
|
+
profile2.command("current").description("Print the current profile name (exits non-zero if none)").action(() => {
|
|
6682
|
+
const current = getCurrentProfileName();
|
|
6683
|
+
if (!current) {
|
|
6684
|
+
console.error(chalk23.dim("No current profile. Set one with `task0 profile use <name>`."));
|
|
6685
|
+
process.exit(1);
|
|
6686
|
+
}
|
|
6687
|
+
console.log(current);
|
|
6688
|
+
});
|
|
6689
|
+
profile2.command("show [name]").description("Show a profile's configuration and detect drift vs daemon.json").action((name) => {
|
|
6690
|
+
const target = name ?? getCurrentProfileName();
|
|
6691
|
+
if (!target) {
|
|
6692
|
+
fail9("No profile name given and no current_profile set. Pass a name or run `task0 profile use <name>` first.");
|
|
6693
|
+
}
|
|
6694
|
+
const entry = getProfile(target);
|
|
6695
|
+
if (!entry) {
|
|
6696
|
+
fail9(`Profile "${target}" not found.`);
|
|
6697
|
+
}
|
|
6698
|
+
const current = getCurrentProfileName();
|
|
6699
|
+
console.log(`${chalk23.bold(target)}${current === target ? chalk23.green(" (current)") : ""}`);
|
|
6700
|
+
console.log(` task0_home: ${entry.task0_home}`);
|
|
6701
|
+
console.log(` api_url: ${entry.api_url ?? chalk23.dim("(unset)")}`);
|
|
6702
|
+
console.log(` config: ${configFilePath()}`);
|
|
6703
|
+
const identity = readDaemonAt(entry.task0_home);
|
|
6704
|
+
if (!identity) {
|
|
6705
|
+
console.log(chalk23.dim(" daemon.json: (not registered yet)"));
|
|
6706
|
+
return;
|
|
6707
|
+
}
|
|
6708
|
+
console.log(` daemon_id: ${identity.daemon_id ?? chalk23.dim("(missing)")}`);
|
|
6709
|
+
console.log(` daemon.server_url: ${identity.server_url ?? chalk23.dim("(missing)")}`);
|
|
6710
|
+
if (entry.api_url && identity.server_url && entry.api_url !== identity.server_url) {
|
|
6711
|
+
console.log(chalk23.yellow(` warn: profile.api_url and daemon.json.server_url disagree.`));
|
|
6712
|
+
console.log(chalk23.dim(` Re-register the daemon to align: task0 --profile ${target} daemon register --server ${entry.api_url}`));
|
|
6713
|
+
}
|
|
6714
|
+
});
|
|
6715
|
+
|
|
5250
6716
|
// src/core/error-capture.ts
|
|
5251
6717
|
init_node();
|
|
5252
6718
|
var DEFAULT_KEEP = 50;
|
|
@@ -5344,17 +6810,8 @@ function captureTopLevel(err, options) {
|
|
|
5344
6810
|
}
|
|
5345
6811
|
|
|
5346
6812
|
// src/main.ts
|
|
5347
|
-
var TASK0_VERSION =
|
|
5348
|
-
|
|
5349
|
-
try {
|
|
5350
|
-
const here = path25.dirname(fileURLToPath2(import.meta.url));
|
|
5351
|
-
const pkg = JSON.parse(readFileSync(path25.resolve(here, "..", "package.json"), "utf-8"));
|
|
5352
|
-
return pkg.version ?? "unknown";
|
|
5353
|
-
} catch {
|
|
5354
|
-
return "unknown";
|
|
5355
|
-
}
|
|
5356
|
-
}
|
|
5357
|
-
var program = new Command23().name("task0").description("Task-centric control layer for agent workflow").version(TASK0_VERSION);
|
|
6813
|
+
var TASK0_VERSION = readCliVersion();
|
|
6814
|
+
var program = new Command24().name("task0").description("Task-centric control layer for agent workflow").version(TASK0_VERSION).option("--profile <name>", "Use a named profile from ~/.config/task0/config.yml (overrides current_profile and TASK0_HOME)");
|
|
5358
6815
|
program.addCommand(source);
|
|
5359
6816
|
program.addCommand(project);
|
|
5360
6817
|
program.addCommand(task);
|
|
@@ -5372,7 +6829,17 @@ program.addCommand(daemonCmd);
|
|
|
5372
6829
|
program.addCommand(userCmd);
|
|
5373
6830
|
program.addCommand(error);
|
|
5374
6831
|
program.addCommand(automation);
|
|
6832
|
+
program.addCommand(profile2);
|
|
5375
6833
|
async function main() {
|
|
6834
|
+
try {
|
|
6835
|
+
activateProfile(process.argv);
|
|
6836
|
+
} catch (err) {
|
|
6837
|
+
if (err instanceof ProfileNotFoundError) {
|
|
6838
|
+
console.error(chalk24.red(err.message));
|
|
6839
|
+
process.exit(1);
|
|
6840
|
+
}
|
|
6841
|
+
throw err;
|
|
6842
|
+
}
|
|
5376
6843
|
installErrorCapture({ version: TASK0_VERSION });
|
|
5377
6844
|
try {
|
|
5378
6845
|
await program.parseAsync(process.argv);
|