@phnx-labs/agents-cli 1.20.57 → 1.20.59
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/CHANGELOG.md +30 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/defaults.js +24 -0
- package/dist/commands/exec.js +29 -5
- package/dist/commands/output.d.ts +19 -0
- package/dist/commands/output.js +333 -0
- package/dist/commands/secrets.js +34 -25
- package/dist/commands/versions.js +11 -3
- package/dist/commands/view.js +19 -4
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +42 -14
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +65 -17
- package/dist/lib/git.d.ts +9 -0
- package/dist/lib/git.js +12 -0
- package/dist/lib/hosts/dispatch.d.ts +21 -0
- package/dist/lib/hosts/dispatch.js +88 -5
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/mcp.js +1 -1
- package/dist/lib/output/git-output.d.ts +74 -0
- package/dist/lib/output/git-output.js +213 -0
- package/dist/lib/permissions.d.ts +31 -1
- package/dist/lib/permissions.js +210 -9
- package/dist/lib/project-root.d.ts +65 -0
- package/dist/lib/project-root.js +134 -0
- package/dist/lib/resources/mcp.js +1 -1
- package/dist/lib/resources/permissions.js +5 -1
- package/dist/lib/resources/skills.js +6 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/secrets/agent.d.ts +26 -23
- package/dist/lib/secrets/agent.js +196 -216
- package/dist/lib/secrets/remote.d.ts +7 -2
- package/dist/lib/secrets/remote.js +12 -10
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +1 -0
- package/dist/lib/session/db.d.ts +3 -0
- package/dist/lib/session/db.js +20 -4
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +40 -4
- package/dist/lib/session/parse.js +38 -15
- package/dist/lib/session/state.d.ts +4 -1
- package/dist/lib/session/state.js +18 -1
- package/dist/lib/session/types.d.ts +10 -0
- package/dist/lib/staleness/detectors/permissions.js +64 -1
- package/dist/lib/staleness/detectors/subagents.js +30 -0
- package/dist/lib/staleness/writers/commands.js +3 -3
- package/dist/lib/staleness/writers/subagents.js +13 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +23 -0
- package/dist/lib/subagents.js +161 -12
- package/dist/lib/teams/agents.d.ts +14 -0
- package/dist/lib/teams/agents.js +158 -22
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/versions.d.ts +39 -0
- package/dist/lib/versions.js +199 -12
- package/package.json +1 -1
- package/scripts/postinstall.js +26 -11
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import { capableAgents } from '../../capabilities.js';
|
|
16
|
-
import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForDroid, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
|
|
16
|
+
import { listInstalledSubagents, transformSubagentForClaude, transformSubagentForCodex, transformSubagentForCopilot, writeKimiSubagentFiles, buildKimiSubagentsParentYaml, KIMI_SUBAGENTS_PARENT_FILE, transformSubagentForOpenCode, transformSubagentForDroid, transformSubagentForKiro, syncSubagentToOpenclaw, parseSubagentFrontmatter, } from '../../subagents.js';
|
|
17
17
|
import { safeJoin } from '../../paths.js';
|
|
18
18
|
import { lazyAgentMap } from './lazy-map.js';
|
|
19
19
|
function buildSubagentsWriter(agent) {
|
|
@@ -57,12 +57,24 @@ function buildSubagentsWriter(agent) {
|
|
|
57
57
|
fs.writeFileSync(safeJoin(droidsDir, `${sub.name}.md`), transformSubagentForDroid(sub.path));
|
|
58
58
|
synced.push(sub.name);
|
|
59
59
|
}
|
|
60
|
+
else if (agent === 'copilot') {
|
|
61
|
+
const agentsDir = path.join(versionHome, '.copilot', 'agents');
|
|
62
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
63
|
+
fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.agent.md`), transformSubagentForCopilot(sub.path));
|
|
64
|
+
synced.push(sub.name);
|
|
65
|
+
}
|
|
60
66
|
else if (agent === 'openclaw') {
|
|
61
67
|
const target = safeJoin(path.join(versionHome, '.openclaw'), sub.name);
|
|
62
68
|
const r = syncSubagentToOpenclaw(sub.path, target);
|
|
63
69
|
if (r.success)
|
|
64
70
|
synced.push(sub.name);
|
|
65
71
|
}
|
|
72
|
+
else if (agent === 'kiro') {
|
|
73
|
+
const agentsDir = path.join(versionHome, '.kiro', 'agents');
|
|
74
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
75
|
+
fs.writeFileSync(safeJoin(agentsDir, `${sub.name}.json`), transformSubagentForKiro(sub.path));
|
|
76
|
+
synced.push(sub.name);
|
|
77
|
+
}
|
|
66
78
|
}
|
|
67
79
|
catch { /* per-item sync failure: skip */ }
|
|
68
80
|
}
|
|
@@ -65,6 +65,7 @@ export declare const loadDrive: ModuleLoader;
|
|
|
65
65
|
export declare const loadFactory: ModuleLoader;
|
|
66
66
|
export declare const loadUsage: ModuleLoader;
|
|
67
67
|
export declare const loadCost: ModuleLoader;
|
|
68
|
+
export declare const loadOutput: ModuleLoader;
|
|
68
69
|
export declare const loadBudget: ModuleLoader;
|
|
69
70
|
export declare const loadAlias: ModuleLoader;
|
|
70
71
|
export declare const loadPty: ModuleLoader;
|
|
@@ -43,6 +43,7 @@ export const loadDrive = async () => (await import('../../commands/drive.js')).r
|
|
|
43
43
|
export const loadFactory = async () => (await import('../../commands/factory.js')).registerFactoryCommands;
|
|
44
44
|
export const loadUsage = async () => (await import('../../commands/usage.js')).registerUsageCommand;
|
|
45
45
|
export const loadCost = async () => (await import('../../commands/cost.js')).registerCostCommand;
|
|
46
|
+
export const loadOutput = async () => (await import('../../commands/output.js')).registerOutputCommand;
|
|
46
47
|
export const loadBudget = async () => (await import('../../commands/budget.js')).registerBudgetCommand;
|
|
47
48
|
export const loadAlias = async () => (await import('../../commands/alias.js')).registerAliasCommand;
|
|
48
49
|
export const loadPty = async () => (await import('../../commands/pty.js')).registerPtyCommands;
|
|
@@ -137,6 +138,7 @@ export const COMMAND_LOADERS = {
|
|
|
137
138
|
factory: [loadFactory],
|
|
138
139
|
usage: [loadUsage],
|
|
139
140
|
cost: [loadCost],
|
|
141
|
+
output: [loadOutput],
|
|
140
142
|
budget: [loadBudget],
|
|
141
143
|
alias: [loadAlias],
|
|
142
144
|
pty: [loadPty],
|
package/dist/lib/subagents.d.ts
CHANGED
|
@@ -56,6 +56,18 @@ export declare function transformSubagentForClaude(subagentDir: string): string;
|
|
|
56
56
|
* See https://docs.factory.ai/cli/configuration/custom-droids.
|
|
57
57
|
*/
|
|
58
58
|
export declare function transformSubagentForDroid(subagentDir: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Transform a subagent into a GitHub Copilot CLI custom agent `.agent.md` file.
|
|
61
|
+
*
|
|
62
|
+
* Copilot custom agents are Markdown profiles with YAML frontmatter stored in
|
|
63
|
+
* `~/.copilot/agents/` (user) or `.github/agents/` (project). The file name
|
|
64
|
+
* ends in `.agent.md` and the frontmatter carries `name`, `description`, and
|
|
65
|
+
* optionally `model` and `tools`. The emitted body is identical to Factory
|
|
66
|
+
* Droid's custom-droid format (flatten frontmatter + body + appended .md
|
|
67
|
+
* sections, `color` dropped), so this is an alias of transformSubagentForDroid.
|
|
68
|
+
* See GitHub docs for custom agents.
|
|
69
|
+
*/
|
|
70
|
+
export declare const transformSubagentForCopilot: typeof transformSubagentForDroid;
|
|
59
71
|
/**
|
|
60
72
|
* Transform a subagent into an OpenCode agent markdown file.
|
|
61
73
|
*
|
|
@@ -107,6 +119,16 @@ export declare function writeKimiSubagentFiles(agentsDir: string, subagentDir: s
|
|
|
107
119
|
* https://developers.openai.com/codex/subagents (custom agents section)
|
|
108
120
|
*/
|
|
109
121
|
export declare function transformSubagentForCodex(subagentDir: string): string;
|
|
122
|
+
/**
|
|
123
|
+
* Transform a subagent into a Kiro CLI custom-agent JSON file.
|
|
124
|
+
*
|
|
125
|
+
* Kiro custom agents live in `~/.kiro/agents/<name>.json` (or `.kiro/agents/`
|
|
126
|
+
* workspace-local) and declare name, description, prompt, tools, and optional
|
|
127
|
+
* model. We flatten the AGENT.md frontmatter + body plus any sibling .md files
|
|
128
|
+
* as sections into a single `prompt`, and expose the standard built-in tool
|
|
129
|
+
* set so the subagent can actually run.
|
|
130
|
+
*/
|
|
131
|
+
export declare function transformSubagentForKiro(subagentDir: string): string;
|
|
110
132
|
/**
|
|
111
133
|
* Sync a subagent to an OpenClaw workspace
|
|
112
134
|
* Copies full directory, renames AGENT.md to AGENTS.md
|
|
@@ -137,6 +159,7 @@ export declare function subagentContentMatches(installedDir: string, sourceDir:
|
|
|
137
159
|
* List subagents installed to a specific agent's home
|
|
138
160
|
* Claude: scans ~/.claude/agents/{name}.md
|
|
139
161
|
* Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
|
|
162
|
+
* Kiro: scans ~/.kiro/agents/{name}.json
|
|
140
163
|
* OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
|
|
141
164
|
*/
|
|
142
165
|
export declare function listSubagentsForAgent(agentId: AgentId, home: string): InstalledSubagent[];
|
package/dist/lib/subagents.js
CHANGED
|
@@ -262,6 +262,18 @@ export function transformSubagentForDroid(subagentDir) {
|
|
|
262
262
|
}
|
|
263
263
|
return result;
|
|
264
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Transform a subagent into a GitHub Copilot CLI custom agent `.agent.md` file.
|
|
267
|
+
*
|
|
268
|
+
* Copilot custom agents are Markdown profiles with YAML frontmatter stored in
|
|
269
|
+
* `~/.copilot/agents/` (user) or `.github/agents/` (project). The file name
|
|
270
|
+
* ends in `.agent.md` and the frontmatter carries `name`, `description`, and
|
|
271
|
+
* optionally `model` and `tools`. The emitted body is identical to Factory
|
|
272
|
+
* Droid's custom-droid format (flatten frontmatter + body + appended .md
|
|
273
|
+
* sections, `color` dropped), so this is an alias of transformSubagentForDroid.
|
|
274
|
+
* See GitHub docs for custom agents.
|
|
275
|
+
*/
|
|
276
|
+
export const transformSubagentForCopilot = transformSubagentForDroid;
|
|
265
277
|
/**
|
|
266
278
|
* Transform a subagent into an OpenCode agent markdown file.
|
|
267
279
|
*
|
|
@@ -381,21 +393,10 @@ export function writeKimiSubagentFiles(agentsDir, subagentDir, name) {
|
|
|
381
393
|
export function transformSubagentForCodex(subagentDir) {
|
|
382
394
|
const agentMd = path.join(subagentDir, 'AGENT.md');
|
|
383
395
|
const frontmatter = parseSubagentFrontmatter(agentMd);
|
|
384
|
-
const body = getSubagentBody(agentMd);
|
|
385
396
|
if (!frontmatter) {
|
|
386
397
|
throw new Error(`Invalid AGENT.md in ${subagentDir}`);
|
|
387
398
|
}
|
|
388
|
-
|
|
389
|
-
let instructions = body.trim();
|
|
390
|
-
const files = fs.readdirSync(subagentDir)
|
|
391
|
-
.filter(f => f.endsWith('.md') && f !== 'AGENT.md')
|
|
392
|
-
.sort();
|
|
393
|
-
for (const file of files) {
|
|
394
|
-
const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
|
|
395
|
-
const sectionName = file.replace('.md', '');
|
|
396
|
-
const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
|
|
397
|
-
instructions += `\n\n## ${title}\n\n${content}`;
|
|
398
|
-
}
|
|
399
|
+
const instructions = flattenSubagentInstructions(subagentDir);
|
|
399
400
|
// Escape TOML multi-line string (""") content — only """ needs escaping.
|
|
400
401
|
const safeInstructions = instructions.replace(/"""/g, '\\"""');
|
|
401
402
|
const safeName = frontmatter.name.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
@@ -409,6 +410,56 @@ export function transformSubagentForCodex(subagentDir) {
|
|
|
409
410
|
toml += `developer_instructions = """\n${safeInstructions}\n"""\n`;
|
|
410
411
|
return toml;
|
|
411
412
|
}
|
|
413
|
+
function flattenSubagentInstructions(subagentDir) {
|
|
414
|
+
let instructions = getSubagentBody(path.join(subagentDir, 'AGENT.md')).trim();
|
|
415
|
+
const files = fs.readdirSync(subagentDir)
|
|
416
|
+
.filter(f => f.endsWith('.md') && f !== 'AGENT.md')
|
|
417
|
+
.sort();
|
|
418
|
+
for (const file of files) {
|
|
419
|
+
const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
|
|
420
|
+
const sectionName = file.replace('.md', '');
|
|
421
|
+
const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
|
|
422
|
+
instructions += `\n\n## ${title}\n\n${content}`;
|
|
423
|
+
}
|
|
424
|
+
return instructions;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Transform a subagent into a Kiro CLI custom-agent JSON file.
|
|
428
|
+
*
|
|
429
|
+
* Kiro custom agents live in `~/.kiro/agents/<name>.json` (or `.kiro/agents/`
|
|
430
|
+
* workspace-local) and declare name, description, prompt, tools, and optional
|
|
431
|
+
* model. We flatten the AGENT.md frontmatter + body plus any sibling .md files
|
|
432
|
+
* as sections into a single `prompt`, and expose the standard built-in tool
|
|
433
|
+
* set so the subagent can actually run.
|
|
434
|
+
*/
|
|
435
|
+
export function transformSubagentForKiro(subagentDir) {
|
|
436
|
+
const agentMd = path.join(subagentDir, 'AGENT.md');
|
|
437
|
+
const frontmatter = parseSubagentFrontmatter(agentMd);
|
|
438
|
+
const body = getSubagentBody(agentMd);
|
|
439
|
+
if (!frontmatter) {
|
|
440
|
+
throw new Error(`Invalid AGENT.md in ${subagentDir}`);
|
|
441
|
+
}
|
|
442
|
+
const files = fs.readdirSync(subagentDir)
|
|
443
|
+
.filter(f => f.endsWith('.md') && f !== 'AGENT.md')
|
|
444
|
+
.sort();
|
|
445
|
+
let prompt = body;
|
|
446
|
+
for (const file of files) {
|
|
447
|
+
const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
|
|
448
|
+
const sectionName = file.replace('.md', '');
|
|
449
|
+
const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
|
|
450
|
+
prompt += `\n\n## ${title}\n\n${content}`;
|
|
451
|
+
}
|
|
452
|
+
const config = {
|
|
453
|
+
name: frontmatter.name,
|
|
454
|
+
description: frontmatter.description,
|
|
455
|
+
prompt,
|
|
456
|
+
tools: ['read', 'write', 'shell', 'web_search', 'web_fetch'],
|
|
457
|
+
};
|
|
458
|
+
if (frontmatter.model) {
|
|
459
|
+
config.model = frontmatter.model;
|
|
460
|
+
}
|
|
461
|
+
return JSON.stringify(config, null, 2);
|
|
462
|
+
}
|
|
412
463
|
/**
|
|
413
464
|
* Sync a subagent to an OpenClaw workspace
|
|
414
465
|
* Copies full directory, renames AGENT.md to AGENTS.md
|
|
@@ -497,6 +548,21 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
|
|
|
497
548
|
const targetDir = safeJoin(path.join(agentHome, '.openclaw'), subagentName);
|
|
498
549
|
return syncSubagentToOpenclaw(subagentDir, targetDir);
|
|
499
550
|
}
|
|
551
|
+
else if (agent === 'kiro') {
|
|
552
|
+
// Kiro: JSON custom-agent file under ~/.kiro/agents/
|
|
553
|
+
const agentsDir = path.join(agentHome, '.kiro', 'agents');
|
|
554
|
+
if (!fs.existsSync(agentsDir)) {
|
|
555
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
556
|
+
}
|
|
557
|
+
try {
|
|
558
|
+
const transformed = transformSubagentForKiro(subagentDir);
|
|
559
|
+
fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.json`), transformed);
|
|
560
|
+
return { success: true };
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
return { success: false, error: String(err) };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
500
566
|
else {
|
|
501
567
|
// Other agents don't support subagents yet
|
|
502
568
|
return { success: false, error: `Agent '${agent}' does not support subagents` };
|
|
@@ -545,6 +611,13 @@ export function removeSubagentFromAgent(subagentName, agent, agentHome) {
|
|
|
545
611
|
}
|
|
546
612
|
return { success: true };
|
|
547
613
|
}
|
|
614
|
+
else if (agent === 'kiro') {
|
|
615
|
+
const targetPath = safeJoin(path.join(agentHome, '.kiro', 'agents'), `${subagentName}.json`);
|
|
616
|
+
if (fs.existsSync(targetPath)) {
|
|
617
|
+
fs.unlinkSync(targetPath);
|
|
618
|
+
}
|
|
619
|
+
return { success: true };
|
|
620
|
+
}
|
|
548
621
|
else {
|
|
549
622
|
return { success: true }; // No-op for unsupported agents
|
|
550
623
|
}
|
|
@@ -584,6 +657,7 @@ export function subagentContentMatches(installedDir, sourceDir) {
|
|
|
584
657
|
* List subagents installed to a specific agent's home
|
|
585
658
|
* Claude: scans ~/.claude/agents/{name}.md
|
|
586
659
|
* Kimi: scans ~/.kimi-code/agents/{name}.yaml (+ sibling .system.md)
|
|
660
|
+
* Kiro: scans ~/.kiro/agents/{name}.json
|
|
587
661
|
* OpenClaw: scans ~/.openclaw/{name}/AGENTS.md
|
|
588
662
|
*/
|
|
589
663
|
export function listSubagentsForAgent(agentId, home) {
|
|
@@ -656,6 +730,22 @@ export function listSubagentsForAgent(agentId, home) {
|
|
|
656
730
|
subagents.push({ name, path: filePath, files: [file], frontmatter });
|
|
657
731
|
}
|
|
658
732
|
}
|
|
733
|
+
else if (agentId === 'copilot') {
|
|
734
|
+
// Copilot: flat `<name>.agent.md` files under ~/.copilot/agents/
|
|
735
|
+
const agentsDir = path.join(home, '.copilot', 'agents');
|
|
736
|
+
if (!fs.existsSync(agentsDir))
|
|
737
|
+
return subagents;
|
|
738
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
739
|
+
if (!file.endsWith('.agent.md'))
|
|
740
|
+
continue;
|
|
741
|
+
const filePath = path.join(agentsDir, file);
|
|
742
|
+
if (!fs.statSync(filePath).isFile())
|
|
743
|
+
continue;
|
|
744
|
+
const name = file.replace(/\.agent\.md$/, '');
|
|
745
|
+
const frontmatter = parseSubagentFrontmatter(filePath) ?? { name, description: '' };
|
|
746
|
+
subagents.push({ name, path: filePath, files: [file], frontmatter });
|
|
747
|
+
}
|
|
748
|
+
}
|
|
659
749
|
else if (agentId === 'openclaw') {
|
|
660
750
|
// OpenClaw: directories with AGENTS.md
|
|
661
751
|
const openclawDir = path.join(home, '.openclaw');
|
|
@@ -697,6 +787,38 @@ export function listSubagentsForAgent(agentId, home) {
|
|
|
697
787
|
});
|
|
698
788
|
}
|
|
699
789
|
}
|
|
790
|
+
else if (agentId === 'kiro') {
|
|
791
|
+
// Kiro: JSON files under ~/.kiro/agents/
|
|
792
|
+
const agentsDir = path.join(home, '.kiro', 'agents');
|
|
793
|
+
if (!fs.existsSync(agentsDir))
|
|
794
|
+
return subagents;
|
|
795
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
796
|
+
if (!file.endsWith('.json'))
|
|
797
|
+
continue;
|
|
798
|
+
const filePath = path.join(agentsDir, file);
|
|
799
|
+
if (!fs.statSync(filePath).isFile())
|
|
800
|
+
continue;
|
|
801
|
+
let frontmatter;
|
|
802
|
+
try {
|
|
803
|
+
const config = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
804
|
+
frontmatter = {
|
|
805
|
+
name: config.name || file.replace('.json', ''),
|
|
806
|
+
description: config.description || '',
|
|
807
|
+
model: config.model,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const name = file.replace('.json', '');
|
|
814
|
+
subagents.push({
|
|
815
|
+
name,
|
|
816
|
+
path: filePath,
|
|
817
|
+
files: [file],
|
|
818
|
+
frontmatter,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
700
822
|
return subagents;
|
|
701
823
|
}
|
|
702
824
|
/**
|
|
@@ -768,6 +890,19 @@ export function diffVersionSubagents(agent, version) {
|
|
|
768
890
|
}
|
|
769
891
|
}
|
|
770
892
|
}
|
|
893
|
+
else if (agent === 'kiro') {
|
|
894
|
+
const agentsDir = path.join(versionHome, '.kiro', 'agents');
|
|
895
|
+
if (fs.existsSync(agentsDir)) {
|
|
896
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
897
|
+
if (!file.endsWith('.json'))
|
|
898
|
+
continue;
|
|
899
|
+
const name = path.basename(file, '.json');
|
|
900
|
+
if (!discovered.has(name)) {
|
|
901
|
+
orphans.push(name);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
771
906
|
return { agent, version, orphans: orphans.sort() };
|
|
772
907
|
}
|
|
773
908
|
/**
|
|
@@ -826,6 +961,13 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
|
|
|
826
961
|
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
|
|
827
962
|
}
|
|
828
963
|
}
|
|
964
|
+
else if (agent === 'copilot') {
|
|
965
|
+
const targetPath = path.join(versionHome, '.copilot', 'agents', `${subagentName}.agent.md`);
|
|
966
|
+
if (fs.existsSync(targetPath)) {
|
|
967
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
968
|
+
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.agent.md.${stamp}`));
|
|
969
|
+
}
|
|
970
|
+
}
|
|
829
971
|
else if (agent === 'openclaw') {
|
|
830
972
|
const targetDir = path.join(versionHome, '.openclaw', subagentName);
|
|
831
973
|
if (fs.existsSync(targetDir)) {
|
|
@@ -834,6 +976,13 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
|
|
|
834
976
|
fs.renameSync(targetDir, trashDest);
|
|
835
977
|
}
|
|
836
978
|
}
|
|
979
|
+
else if (agent === 'kiro') {
|
|
980
|
+
const targetPath = path.join(versionHome, '.kiro', 'agents', `${subagentName}.json`);
|
|
981
|
+
if (fs.existsSync(targetPath)) {
|
|
982
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
983
|
+
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.json.${stamp}`));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
837
986
|
return { success: true };
|
|
838
987
|
}
|
|
839
988
|
catch (err) {
|
|
@@ -213,6 +213,10 @@ export declare class AgentProcess {
|
|
|
213
213
|
* round-trips so a bare `teams status`/`teams logs` is still correct.
|
|
214
214
|
*/
|
|
215
215
|
private syncRemoteMirror;
|
|
216
|
+
/** Reset the local stdout cursor for a newly truncated resume log. */
|
|
217
|
+
resetLogReadPosition(): number;
|
|
218
|
+
/** Restore the cursor when a resume transaction puts the prior log back. */
|
|
219
|
+
restoreLogReadPosition(position: number): void;
|
|
216
220
|
readNewEvents(): Promise<void>;
|
|
217
221
|
/**
|
|
218
222
|
* Truncate the local mirror to its trailing REMOTE_MIRROR_MAX_BYTES and reset
|
|
@@ -268,6 +272,16 @@ export declare class AgentProcess {
|
|
|
268
272
|
export type CloudDispatchFn = (agent: AgentProcess) => Promise<{
|
|
269
273
|
cloudSessionId: string;
|
|
270
274
|
}>;
|
|
275
|
+
interface ResumeLogTransaction {
|
|
276
|
+
agent: AgentProcess;
|
|
277
|
+
stdoutPath: string;
|
|
278
|
+
backupPath: string;
|
|
279
|
+
hadOriginal: boolean;
|
|
280
|
+
previousReadPos: number;
|
|
281
|
+
}
|
|
282
|
+
export declare function beginResumeLogTransaction(agent: AgentProcess): Promise<ResumeLogTransaction>;
|
|
283
|
+
export declare function commitResumeLogTransaction(transaction: ResumeLogTransaction): Promise<void>;
|
|
284
|
+
export declare function terminateSpawnedProcess(pid: number): Promise<void>;
|
|
271
285
|
export declare class AgentManager {
|
|
272
286
|
private agents;
|
|
273
287
|
private maxAgents;
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -27,7 +27,7 @@ import { recordRunName } from '../session/run-names.js';
|
|
|
27
27
|
import { sshExec, shellQuote } from '../ssh-exec.js';
|
|
28
28
|
import { resolveHost } from '../hosts/registry.js';
|
|
29
29
|
import { sshTargetFor } from '../hosts/types.js';
|
|
30
|
-
import { dispatchAgentsCommand } from '../hosts/dispatch.js';
|
|
30
|
+
import { dispatchAgentsCommand, terminateDispatchedTask } from '../hosts/dispatch.js';
|
|
31
31
|
import { ensureHostReady } from '../hosts/ready.js';
|
|
32
32
|
import { remoteShellFor } from '../hosts/remote-cmd.js';
|
|
33
33
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
@@ -746,6 +746,16 @@ export class AgentProcess {
|
|
|
746
746
|
}
|
|
747
747
|
}
|
|
748
748
|
}
|
|
749
|
+
/** Reset the local stdout cursor for a newly truncated resume log. */
|
|
750
|
+
resetLogReadPosition() {
|
|
751
|
+
const previous = this.lastReadPos;
|
|
752
|
+
this.lastReadPos = 0;
|
|
753
|
+
return previous;
|
|
754
|
+
}
|
|
755
|
+
/** Restore the cursor when a resume transaction puts the prior log back. */
|
|
756
|
+
restoreLogReadPosition(position) {
|
|
757
|
+
this.lastReadPos = position;
|
|
758
|
+
}
|
|
749
759
|
async readNewEvents() {
|
|
750
760
|
// Distributed teammate: mirror the host's new log bytes locally first, then
|
|
751
761
|
// fall through to the identical local read+parse below.
|
|
@@ -1107,6 +1117,62 @@ export class AgentProcess {
|
|
|
1107
1117
|
}
|
|
1108
1118
|
}
|
|
1109
1119
|
}
|
|
1120
|
+
export async function beginResumeLogTransaction(agent) {
|
|
1121
|
+
const stdoutPath = await agent.getStdoutPath();
|
|
1122
|
+
const backupPath = `${stdoutPath}.resume-backup-${randomUUID()}`;
|
|
1123
|
+
let hadOriginal = false;
|
|
1124
|
+
try {
|
|
1125
|
+
await fs.rename(stdoutPath, backupPath);
|
|
1126
|
+
hadOriginal = true;
|
|
1127
|
+
}
|
|
1128
|
+
catch (err) {
|
|
1129
|
+
if (err?.code !== 'ENOENT')
|
|
1130
|
+
throw err;
|
|
1131
|
+
}
|
|
1132
|
+
const previousReadPos = agent.resetLogReadPosition();
|
|
1133
|
+
return { agent, stdoutPath, backupPath, hadOriginal, previousReadPos };
|
|
1134
|
+
}
|
|
1135
|
+
export async function commitResumeLogTransaction(transaction) {
|
|
1136
|
+
if (transaction.hadOriginal)
|
|
1137
|
+
await fs.rm(transaction.backupPath, { force: true });
|
|
1138
|
+
}
|
|
1139
|
+
async function rollbackResumeLogTransaction(transaction) {
|
|
1140
|
+
try {
|
|
1141
|
+
await fs.rm(transaction.stdoutPath, { force: true });
|
|
1142
|
+
if (transaction.hadOriginal) {
|
|
1143
|
+
await fs.rename(transaction.backupPath, transaction.stdoutPath);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
finally {
|
|
1147
|
+
transaction.agent.restoreLogReadPosition(transaction.previousReadPos);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
export async function terminateSpawnedProcess(pid) {
|
|
1151
|
+
try {
|
|
1152
|
+
process.kill(-pid, 'SIGTERM');
|
|
1153
|
+
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
if (err?.code === 'ESRCH')
|
|
1156
|
+
return;
|
|
1157
|
+
throw err;
|
|
1158
|
+
}
|
|
1159
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
1160
|
+
try {
|
|
1161
|
+
process.kill(-pid, 0);
|
|
1162
|
+
}
|
|
1163
|
+
catch (err) {
|
|
1164
|
+
if (err?.code === 'ESRCH')
|
|
1165
|
+
return;
|
|
1166
|
+
throw err;
|
|
1167
|
+
}
|
|
1168
|
+
try {
|
|
1169
|
+
process.kill(-pid, 'SIGKILL');
|
|
1170
|
+
}
|
|
1171
|
+
catch (err) {
|
|
1172
|
+
if (err?.code !== 'ESRCH')
|
|
1173
|
+
throw err;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1110
1176
|
export class AgentManager {
|
|
1111
1177
|
agents = new Map();
|
|
1112
1178
|
maxAgents;
|
|
@@ -1402,16 +1468,42 @@ export class AgentManager {
|
|
|
1402
1468
|
`(it may have failed before its first turn). Start a fresh teammate instead.`);
|
|
1403
1469
|
}
|
|
1404
1470
|
const resume = { id: agent.remoteSessionId ?? agent.agentId, message };
|
|
1471
|
+
const priorRuntime = {
|
|
1472
|
+
status: agent.status,
|
|
1473
|
+
completedAt: agent.completedAt,
|
|
1474
|
+
pid: agent.pid,
|
|
1475
|
+
startTime: agent.startTime,
|
|
1476
|
+
startedAt: agent.startedAt,
|
|
1477
|
+
remotePid: agent.remotePid,
|
|
1478
|
+
remoteLog: agent.remoteLog,
|
|
1479
|
+
remoteExit: agent.remoteExit,
|
|
1480
|
+
remoteLogOffset: agent.remoteLogOffset,
|
|
1481
|
+
worktreePath: agent.worktreePath,
|
|
1482
|
+
};
|
|
1405
1483
|
// Flip to RUNNING up front so a concurrent status poll can't reap the
|
|
1406
1484
|
// teammate between the exit-sentinel clear and the new PID landing; the
|
|
1407
|
-
// launch re-persists with the fresh pid/startTime.
|
|
1485
|
+
// launch re-persists with the fresh pid/startTime. If relaunch fails before
|
|
1486
|
+
// that happens, restore the stopped lifecycle state and keep its existing
|
|
1487
|
+
// metadata/log directory intact so the user can retry.
|
|
1408
1488
|
agent.status = AgentStatus.RUNNING;
|
|
1409
1489
|
agent.completedAt = null;
|
|
1410
|
-
|
|
1411
|
-
|
|
1490
|
+
try {
|
|
1491
|
+
if (agent.hostName) {
|
|
1492
|
+
await this.launchRemoteProcess(agent, resume);
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
await this.launchProcess(agent, resume);
|
|
1496
|
+
}
|
|
1412
1497
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1498
|
+
catch (err) {
|
|
1499
|
+
Object.assign(agent, priorRuntime);
|
|
1500
|
+
try {
|
|
1501
|
+
await agent.saveMeta();
|
|
1502
|
+
}
|
|
1503
|
+
catch (restoreErr) {
|
|
1504
|
+
throw new Error(`Failed to resume teammate: ${err.message}; restoring stopped state also failed: ${restoreErr.message}`, { cause: err });
|
|
1505
|
+
}
|
|
1506
|
+
throw err;
|
|
1415
1507
|
}
|
|
1416
1508
|
return agent;
|
|
1417
1509
|
}
|
|
@@ -1429,8 +1521,13 @@ export class AgentManager {
|
|
|
1429
1521
|
const resolvedModel = agent.model ?? null;
|
|
1430
1522
|
const cmd = this.buildCommand(agent.agentType, agent.prompt, agent.mode, resolvedModel, agent.cwd, agent.agentId, effort, agent.version, agent.profileName, resume);
|
|
1431
1523
|
debug(`Launching ${agent.agentType} agent ${agent.agentId} [${agent.mode}]${resume ? ' (resume)' : ''}: ${cmd.slice(0, 3).join(' ')}...`);
|
|
1524
|
+
let childProcess = null;
|
|
1525
|
+
let stdoutFile = null;
|
|
1526
|
+
let resumeLog = null;
|
|
1432
1527
|
try {
|
|
1433
|
-
|
|
1528
|
+
if (resume)
|
|
1529
|
+
resumeLog = await beginResumeLogTransaction(agent);
|
|
1530
|
+
const stdoutPath = resumeLog?.stdoutPath ?? await agent.getStdoutPath();
|
|
1434
1531
|
// Always TRUNCATE — including on resume. The status reader re-reads the
|
|
1435
1532
|
// whole log from byte 0 every poll (lastReadPos is in-memory, not
|
|
1436
1533
|
// persisted) and marks terminal status from the last `result` event it
|
|
@@ -1441,7 +1538,7 @@ export class AgentManager {
|
|
|
1441
1538
|
// a forked session. Truncating keeps exactly one turn in the log, so the
|
|
1442
1539
|
// re-read is always correct. The authoritative transcript lives in the
|
|
1443
1540
|
// agent's own session (resumed via --resume), not this stdout mirror.
|
|
1444
|
-
|
|
1541
|
+
stdoutFile = await fs.open(stdoutPath, 'w');
|
|
1445
1542
|
const stdoutFd = stdoutFile.fd;
|
|
1446
1543
|
// Wrap the teammate command in a shell that records the underlying CLI's
|
|
1447
1544
|
// exit code to a sentinel file. Detached + unref()'d children can't be
|
|
@@ -1455,7 +1552,7 @@ export class AgentManager {
|
|
|
1455
1552
|
const wrappedCmd = buildSentinelCommand(cmd, exitCodePath);
|
|
1456
1553
|
// detached:true makes the shell the process-group leader, so stop()'s
|
|
1457
1554
|
// `kill(-pid)` still reaches the underlying CLI through the group.
|
|
1458
|
-
|
|
1555
|
+
childProcess = spawn('/bin/sh', ['-c', wrappedCmd], {
|
|
1459
1556
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
1460
1557
|
cwd: agent.cwd || undefined,
|
|
1461
1558
|
detached: true,
|
|
@@ -1463,8 +1560,13 @@ export class AgentManager {
|
|
|
1463
1560
|
? { ...sanitizeProcessEnv(process.env), ...agent.envOverrides }
|
|
1464
1561
|
: sanitizeProcessEnv(process.env),
|
|
1465
1562
|
});
|
|
1563
|
+
await new Promise((resolve, reject) => {
|
|
1564
|
+
childProcess.once('spawn', resolve);
|
|
1565
|
+
childProcess.once('error', reject);
|
|
1566
|
+
});
|
|
1466
1567
|
childProcess.unref();
|
|
1467
|
-
stdoutFile.close()
|
|
1568
|
+
await stdoutFile.close();
|
|
1569
|
+
stdoutFile = null;
|
|
1468
1570
|
agent.pid = childProcess.pid || null;
|
|
1469
1571
|
// Capture start-time NOW, while we know the PID is ours. Once the
|
|
1470
1572
|
// OS reuses this PID slot, /proc and `ps` will report a different
|
|
@@ -1474,9 +1576,21 @@ export class AgentManager {
|
|
|
1474
1576
|
agent.status = AgentStatus.RUNNING;
|
|
1475
1577
|
agent.startedAt = new Date();
|
|
1476
1578
|
await agent.saveMeta();
|
|
1579
|
+
if (resumeLog)
|
|
1580
|
+
await commitResumeLogTransaction(resumeLog);
|
|
1477
1581
|
}
|
|
1478
1582
|
catch (err) {
|
|
1479
|
-
|
|
1583
|
+
if (stdoutFile)
|
|
1584
|
+
await stdoutFile.close().catch(() => { });
|
|
1585
|
+
if (childProcess?.pid)
|
|
1586
|
+
await terminateSpawnedProcess(childProcess.pid);
|
|
1587
|
+
if (resumeLog)
|
|
1588
|
+
await rollbackResumeLogTransaction(resumeLog);
|
|
1589
|
+
// Fresh spawns own a newly-created directory, so a failed launch removes
|
|
1590
|
+
// that partial record. A resume reuses an existing teammate: its caller
|
|
1591
|
+
// restores the prior terminal state and preserves the directory for retry.
|
|
1592
|
+
if (!resume)
|
|
1593
|
+
await this.cleanupPartialAgent(agent);
|
|
1480
1594
|
console.error(`Failed to spawn agent ${agent.agentId}:`, err);
|
|
1481
1595
|
throw new Error(`Failed to spawn agent: ${err.message}`);
|
|
1482
1596
|
}
|
|
@@ -1535,12 +1649,19 @@ export class AgentManager {
|
|
|
1535
1649
|
// — the supervisor polls the host, we don't block here.
|
|
1536
1650
|
const effort = agent.effort ?? 'medium';
|
|
1537
1651
|
const forwardedArgs = this.buildRunArgv(agent.agentType, agent.prompt, agent.mode, agent.model ?? null, effort, agent.version, agent.profileName, resume);
|
|
1652
|
+
let dispatchedTask = null;
|
|
1653
|
+
let resumeLog = null;
|
|
1538
1654
|
try {
|
|
1655
|
+
if (resume) {
|
|
1656
|
+
resumeLog = await beginResumeLogTransaction(agent);
|
|
1657
|
+
await fs.writeFile(resumeLog.stdoutPath, '');
|
|
1658
|
+
}
|
|
1539
1659
|
const { task } = await dispatchAgentsCommand(host, {
|
|
1540
1660
|
forwardedArgs,
|
|
1541
1661
|
remoteCwd,
|
|
1542
1662
|
follow: false,
|
|
1543
1663
|
});
|
|
1664
|
+
dispatchedTask = task;
|
|
1544
1665
|
agent.remotePid = task.pid ?? null;
|
|
1545
1666
|
agent.remoteLog = task.remoteLog ?? null;
|
|
1546
1667
|
agent.remoteExit = task.remoteExit ?? null;
|
|
@@ -1549,15 +1670,34 @@ export class AgentManager {
|
|
|
1549
1670
|
// syncRemoteMirror appends the delta onto the local mirror. Truncate that
|
|
1550
1671
|
// mirror first so the prior turn's terminal event can't linger and get
|
|
1551
1672
|
// re-read as the current status (same hazard the local path truncates for).
|
|
1552
|
-
if (resume) {
|
|
1553
|
-
await fs.writeFile(await agent.getStdoutPath(), '').catch(() => { });
|
|
1554
|
-
}
|
|
1555
1673
|
agent.status = AgentStatus.RUNNING;
|
|
1556
1674
|
agent.startedAt = new Date();
|
|
1557
1675
|
await agent.saveMeta();
|
|
1676
|
+
if (resumeLog)
|
|
1677
|
+
await commitResumeLogTransaction(resumeLog);
|
|
1558
1678
|
}
|
|
1559
1679
|
catch (err) {
|
|
1680
|
+
let cleanupError = null;
|
|
1681
|
+
if (dispatchedTask) {
|
|
1682
|
+
try {
|
|
1683
|
+
terminateDispatchedTask(dispatchedTask);
|
|
1684
|
+
}
|
|
1685
|
+
catch (cleanupErr) {
|
|
1686
|
+
cleanupError = cleanupErr;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (resumeLog) {
|
|
1690
|
+
try {
|
|
1691
|
+
await rollbackResumeLogTransaction(resumeLog);
|
|
1692
|
+
}
|
|
1693
|
+
catch (cleanupErr) {
|
|
1694
|
+
cleanupError = cleanupError ?? cleanupErr;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1560
1697
|
console.error(`Failed to launch remote teammate ${agent.agentId} on ${agent.hostName}:`, err);
|
|
1698
|
+
if (cleanupError) {
|
|
1699
|
+
throw new Error(`Failed to launch remote teammate: ${err.message}; cleanup failed: ${cleanupError.message}`, { cause: err });
|
|
1700
|
+
}
|
|
1561
1701
|
throw new Error(`Failed to launch remote teammate: ${err.message}`);
|
|
1562
1702
|
}
|
|
1563
1703
|
debug(`Launched remote agent ${agent.agentId} on ${agent.hostName} (remote pid ${agent.remotePid})`);
|
|
@@ -1910,17 +2050,13 @@ export class AgentManager {
|
|
|
1910
2050
|
if (!agent) {
|
|
1911
2051
|
return false;
|
|
1912
2052
|
}
|
|
1913
|
-
// Distributed teammate: no local PID — signal
|
|
1914
|
-
//
|
|
1915
|
-
//
|
|
1916
|
-
// `nohup bash -lc … &` under a non-interactive shell where job control is off,
|
|
1917
|
-
// so `&` may NOT open a new group — fall back to signalling the wrapper pid
|
|
1918
|
-
// directly. Best-effort either way; the `.exit` sentinel is the durable
|
|
1919
|
-
// terminal-status source if a grandchild lingers.
|
|
2053
|
+
// Distributed teammate: no local PID — signal the dedicated process group
|
|
2054
|
+
// created by dispatch.ts. The persisted PID is the group leader, so one
|
|
2055
|
+
// negative-PID signal reaches the login-shell wrapper and every descendant.
|
|
1920
2056
|
if (agent.hostName && agent.status === AgentStatus.RUNNING) {
|
|
1921
2057
|
if (agent.hostTarget && agent.remotePid) {
|
|
1922
2058
|
try {
|
|
1923
|
-
sshExec(agent.hostTarget, `kill -TERM -- -${agent.remotePid} 2>/dev/null
|
|
2059
|
+
sshExec(agent.hostTarget, `kill -TERM -- -${agent.remotePid} 2>/dev/null`, {
|
|
1924
2060
|
timeoutMs: 10000,
|
|
1925
2061
|
multiplex: true,
|
|
1926
2062
|
});
|