@parallel-cli/parallel 0.4.8 → 0.5.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/CHANGELOG.md +57 -0
- package/README.md +43 -7
- package/dist/agents/agent.js +213 -31
- package/dist/agents/execution-policy.js +58 -0
- package/dist/agents/tools.js +83 -22
- package/dist/commands.js +67 -5
- package/dist/config.js +5 -2
- package/dist/controller.js +229 -11
- package/dist/coordination/blackboard.js +8 -7
- package/dist/diagnostics.js +209 -0
- package/dist/i18n.js +60 -4
- package/dist/index.js +31 -7
- package/dist/llm/client.js +7 -3
- package/dist/project-context.js +477 -0
- package/dist/project-index.js +186 -0
- package/dist/security.js +93 -0
- package/dist/server.js +41 -2
- package/dist/ui/AgentPanel.js +6 -2
- package/dist/ui/App.js +4 -2
- package/dist/ui/AttachApp.js +6 -3
- package/dist/ui/CommandInput.js +9 -0
- package/dist/ui/SettingsPanel.js +22 -23
- package/dist/ui/Timeline.js +3 -0
- package/dist/ui/Wizard.js +49 -21
- package/dist/ui/events.js +4 -0
- package/dist/ui/views.js +4 -2
- package/dist/update.js +3 -2
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const EXECUTION_BUDGETS = {
|
|
2
|
+
quick: {
|
|
3
|
+
profile: 'quick',
|
|
4
|
+
maxRounds: 6,
|
|
5
|
+
maxToolCalls: 12,
|
|
6
|
+
maxShellCommands: 2,
|
|
7
|
+
maxInputTokens: 150_000,
|
|
8
|
+
maxResultChars: 8_000,
|
|
9
|
+
maxRecentMessages: 24,
|
|
10
|
+
convergenceAt: 0.7,
|
|
11
|
+
},
|
|
12
|
+
standard: {
|
|
13
|
+
profile: 'standard',
|
|
14
|
+
maxRounds: 16,
|
|
15
|
+
maxToolCalls: 32,
|
|
16
|
+
maxShellCommands: 6,
|
|
17
|
+
maxInputTokens: 600_000,
|
|
18
|
+
maxResultChars: 16_000,
|
|
19
|
+
maxRecentMessages: 42,
|
|
20
|
+
convergenceAt: 0.75,
|
|
21
|
+
},
|
|
22
|
+
deep: {
|
|
23
|
+
profile: 'deep',
|
|
24
|
+
maxRounds: 60,
|
|
25
|
+
maxToolCalls: 120,
|
|
26
|
+
maxShellCommands: 30,
|
|
27
|
+
maxInputTokens: 3_000_000,
|
|
28
|
+
maxResultChars: 32_000,
|
|
29
|
+
maxRecentMessages: 80,
|
|
30
|
+
convergenceAt: 0.82,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const COMPLEX = /\b(migrat|refactor|architecture|redesign|rewrite|exhaustive|end[- ]to[- ]end|across|monorepo|multi[- ]service|security audit|performance audit|release|deploy)\b/i;
|
|
34
|
+
const SIMPLE = /\b(explain|find|locate|where|why|diagnos|inspect|verify|check|typo|rename|toggle|small|simple)\b/i;
|
|
35
|
+
export function classifyExecutionProfile(task, mode, forced) {
|
|
36
|
+
if (forced)
|
|
37
|
+
return forced;
|
|
38
|
+
if (mode === 'plan')
|
|
39
|
+
return 'deep';
|
|
40
|
+
if (mode === 'ask')
|
|
41
|
+
return COMPLEX.test(task) || task.length > 1_200 ? 'standard' : 'quick';
|
|
42
|
+
const pathMentions = task.match(/\b[\w./-]+\.(?:ts|tsx|js|mjs|json|md|py|rs|go|java)\b/g)?.length ?? 0;
|
|
43
|
+
if (COMPLEX.test(task) || pathMentions > 3 || task.length > 1_600)
|
|
44
|
+
return 'standard';
|
|
45
|
+
if (SIMPLE.test(task) || pathMentions <= 1 || task.length < 500)
|
|
46
|
+
return 'quick';
|
|
47
|
+
return 'standard';
|
|
48
|
+
}
|
|
49
|
+
export function nextExecutionProfile(profile) {
|
|
50
|
+
if (profile === 'quick')
|
|
51
|
+
return 'standard';
|
|
52
|
+
if (profile === 'standard')
|
|
53
|
+
return 'deep';
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
export function shouldEscalateExecution(task, inspectedFiles, changedFiles) {
|
|
57
|
+
return COMPLEX.test(task) || inspectedFiles > 3 || changedFiles > 3;
|
|
58
|
+
}
|
package/dist/agents/tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { exec } from 'node:child_process';
|
|
4
4
|
import * as Diff from 'diff';
|
|
5
|
+
import { appendFilePrivate, ensurePrivateDir, sanitizeTerminalText, writeFileAtomicPrivate } from '../security.js';
|
|
5
6
|
const IGNORED = new Set(['node_modules', '.git', '.parallel', '.cursor', 'dist', '__pycache__', '.venv', 'venv']);
|
|
6
7
|
const MAX_OUTPUT = 12_000;
|
|
7
8
|
const MUTATING_TOOLS = new Set(['write_file', 'edit_file', 'claim_files', 'remember']);
|
|
@@ -15,6 +16,12 @@ function isMutatingShell(command) {
|
|
|
15
16
|
return true;
|
|
16
17
|
if (/[>|]\s*(sh|bash)\b/.test(c) || /\b(curl|wget)\b.*\|\s*(sh|bash)/.test(c))
|
|
17
18
|
return true;
|
|
19
|
+
if (/\b(curl|wget)\b.*(&&|;)\s*(sh|bash|zsh|python|node)\b/.test(c))
|
|
20
|
+
return true;
|
|
21
|
+
if (/\b(nc|ncat|netcat|socat|telnet|ssh|scp|rsync)\b/.test(c))
|
|
22
|
+
return true;
|
|
23
|
+
if (/\b(bash|sh|zsh)\s+-c\b|\b(python|python3|node|perl|ruby)\s+(-c|-e)\b/.test(c))
|
|
24
|
+
return true;
|
|
18
25
|
return false;
|
|
19
26
|
}
|
|
20
27
|
export const TOOL_DEFINITIONS = [
|
|
@@ -22,7 +29,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
22
29
|
type: 'function',
|
|
23
30
|
function: {
|
|
24
31
|
name: 'list_files',
|
|
25
|
-
description: 'List project files (recursive, ignores node_modules/.git/dist). Use
|
|
32
|
+
description: 'List project files (recursive, ignores node_modules/.git/dist). Use only when shared project context does not already identify the relevant area.',
|
|
26
33
|
parameters: {
|
|
27
34
|
type: 'object',
|
|
28
35
|
properties: {
|
|
@@ -40,6 +47,8 @@ export const TOOL_DEFINITIONS = [
|
|
|
40
47
|
type: 'object',
|
|
41
48
|
properties: {
|
|
42
49
|
path: { type: 'string', description: 'Relative file path' },
|
|
50
|
+
startLine: { type: 'integer', description: 'Optional first line, 1-based' },
|
|
51
|
+
endLine: { type: 'integer', description: 'Optional last line, inclusive' },
|
|
43
52
|
},
|
|
44
53
|
required: ['path'],
|
|
45
54
|
},
|
|
@@ -63,6 +72,22 @@ export const TOOL_DEFINITIONS = [
|
|
|
63
72
|
},
|
|
64
73
|
},
|
|
65
74
|
},
|
|
75
|
+
{
|
|
76
|
+
type: 'function',
|
|
77
|
+
function: {
|
|
78
|
+
name: 'read_artifact',
|
|
79
|
+
description: 'Read a targeted line range from a long tool output previously stored as an artifact.',
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
id: { type: 'string', description: 'Artifact id returned by a previous tool result' },
|
|
84
|
+
startLine: { type: 'integer', description: 'First line, 1-based' },
|
|
85
|
+
lineCount: { type: 'integer', description: 'Number of lines, max 200' },
|
|
86
|
+
},
|
|
87
|
+
required: ['id'],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
66
91
|
{
|
|
67
92
|
type: 'function',
|
|
68
93
|
function: {
|
|
@@ -113,7 +138,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
113
138
|
type: 'function',
|
|
114
139
|
function: {
|
|
115
140
|
name: 'inspect_project',
|
|
116
|
-
description: 'Batch read-only inspection: list files under paths and/or run several regex searches in one call. Prefer this over cascades of grep/head/tail/wc/awk
|
|
141
|
+
description: 'Batch targeted read-only inspection: list files under task-relevant paths and/or run several regex searches in one call. Prefer this over generic repository exploration or cascades of grep/head/tail/wc/awk.',
|
|
117
142
|
parameters: {
|
|
118
143
|
type: 'object',
|
|
119
144
|
properties: {
|
|
@@ -178,7 +203,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
178
203
|
type: 'function',
|
|
179
204
|
function: {
|
|
180
205
|
name: 'update_steps',
|
|
181
|
-
description: 'Update the visible Cursor-style task checklist. At task start create 3-6
|
|
206
|
+
description: 'Update the visible Cursor-style task checklist. At task start create 3-6 outcome-oriented steps; do not add a generic project-exploration step when shared context already covers it. Keep exactly one active step.',
|
|
182
207
|
parameters: {
|
|
183
208
|
type: 'object',
|
|
184
209
|
properties: {
|
|
@@ -308,12 +333,15 @@ export class ToolExecutor {
|
|
|
308
333
|
requestQuestion;
|
|
309
334
|
skills;
|
|
310
335
|
mode;
|
|
336
|
+
onInspect;
|
|
337
|
+
profile;
|
|
338
|
+
maxOutput;
|
|
311
339
|
/** Last content this agent has seen for each file — basis of adaptive merging. */
|
|
312
340
|
lastRead = new Map();
|
|
313
341
|
/** Questions already asked — capped at 3 per task. */
|
|
314
342
|
questionsAsked = 0;
|
|
315
343
|
planApproved = false;
|
|
316
|
-
constructor(board, agentId, agentName, projectRoot, requestApproval, requestQuestion, skills, mode = 'task') {
|
|
344
|
+
constructor(board, agentId, agentName, projectRoot, requestApproval, requestQuestion, skills, mode = 'task', onInspect, profile = 'standard', maxOutput = MAX_OUTPUT) {
|
|
317
345
|
this.board = board;
|
|
318
346
|
this.agentId = agentId;
|
|
319
347
|
this.agentName = agentName;
|
|
@@ -322,6 +350,9 @@ export class ToolExecutor {
|
|
|
322
350
|
this.requestQuestion = requestQuestion;
|
|
323
351
|
this.skills = skills;
|
|
324
352
|
this.mode = mode;
|
|
353
|
+
this.onInspect = onInspect;
|
|
354
|
+
this.profile = profile;
|
|
355
|
+
this.maxOutput = maxOutput;
|
|
325
356
|
}
|
|
326
357
|
resolve(rel) {
|
|
327
358
|
const root = path.resolve(this.projectRoot);
|
|
@@ -337,6 +368,7 @@ export class ToolExecutor {
|
|
|
337
368
|
}
|
|
338
369
|
rememberRead(relPath, content) {
|
|
339
370
|
this.lastRead.set(relPath, { content, revision: this.board.fileRevision(relPath) });
|
|
371
|
+
this.onInspect?.(this.agentId, relPath, content);
|
|
340
372
|
}
|
|
341
373
|
adaptationMessage(relPath, seen, current, verb = 'was modified') {
|
|
342
374
|
this.board.recordConflict(relPath);
|
|
@@ -414,9 +446,11 @@ export class ToolExecutor {
|
|
|
414
446
|
case 'list_files':
|
|
415
447
|
return this.listFiles(args?.path ?? '.');
|
|
416
448
|
case 'read_file':
|
|
417
|
-
return this.readFile(args.path);
|
|
449
|
+
return this.readFile(args.path, args?.startLine, args?.endLine);
|
|
418
450
|
case 'read_many':
|
|
419
451
|
return this.readMany(args.paths);
|
|
452
|
+
case 'read_artifact':
|
|
453
|
+
return this.readArtifact(args.id, args?.startLine, args?.lineCount);
|
|
420
454
|
case 'write_file':
|
|
421
455
|
return this.writeFile(args.path, args.content);
|
|
422
456
|
case 'edit_file':
|
|
@@ -540,12 +574,12 @@ export class ToolExecutor {
|
|
|
540
574
|
if (!f)
|
|
541
575
|
return 'ERROR: remember needs a non-empty fact.';
|
|
542
576
|
const file = path.join(this.projectRoot, '.parallel', 'memory.md');
|
|
543
|
-
|
|
577
|
+
ensurePrivateDir(path.dirname(file));
|
|
544
578
|
if (!fs.existsSync(file)) {
|
|
545
|
-
|
|
579
|
+
writeFileAtomicPrivate(file, '# Project memory\n\nDurable facts agents recorded. Injected into every agent\'s system prompt.\n\n');
|
|
546
580
|
}
|
|
547
581
|
const line = `- ${f} _(${this.agentName}, ${new Date().toISOString().slice(0, 10)})_\n`;
|
|
548
|
-
|
|
582
|
+
appendFilePrivate(file, line);
|
|
549
583
|
this.board.log(this.agentId, 'tool', `🧠 remember: ${f.slice(0, 80)}`);
|
|
550
584
|
return 'Fact saved to the project memory. Every future agent will see it.';
|
|
551
585
|
}
|
|
@@ -615,16 +649,18 @@ export class ToolExecutor {
|
|
|
615
649
|
return '(empty folder)';
|
|
616
650
|
return out.join('\n');
|
|
617
651
|
}
|
|
618
|
-
readFile(rel) {
|
|
652
|
+
readFile(rel, requestedStart, requestedEnd) {
|
|
619
653
|
const abs = this.resolve(rel);
|
|
620
654
|
const relPath = this.relOf(rel);
|
|
621
655
|
const content = fs.readFileSync(abs, 'utf8');
|
|
622
656
|
this.rememberRead(relPath, content);
|
|
623
657
|
this.board.resolveConflict(relPath);
|
|
624
658
|
const lines = content.split('\n');
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
|
|
659
|
+
const start = Math.max(1, Math.floor(Number(requestedStart) || 1));
|
|
660
|
+
const end = Math.min(lines.length, Math.max(start, Math.floor(Number(requestedEnd) || lines.length)));
|
|
661
|
+
const numbered = lines.slice(start - 1, end).map((l, i) => `${String(start + i).padStart(4)}|${l}`).join('\n');
|
|
662
|
+
return numbered.length > this.maxOutput
|
|
663
|
+
? numbered.slice(0, this.maxOutput) + `\n... (truncated; requested ${start}-${end}, ${lines.length} lines total)`
|
|
628
664
|
: numbered;
|
|
629
665
|
}
|
|
630
666
|
readMany(paths) {
|
|
@@ -641,8 +677,9 @@ export class ToolExecutor {
|
|
|
641
677
|
this.board.resolveConflict(relPath);
|
|
642
678
|
const lines = content.split('\n');
|
|
643
679
|
const numbered = lines.map((l, i) => `${String(i + 1).padStart(4)}|${l}`).join('\n');
|
|
644
|
-
const
|
|
645
|
-
|
|
680
|
+
const perFile = Math.max(1_000, Math.floor(this.maxOutput / relPaths.length));
|
|
681
|
+
const body = numbered.length > perFile
|
|
682
|
+
? numbered.slice(0, perFile) + `\n... (truncated, ${lines.length} lines total)`
|
|
646
683
|
: numbered;
|
|
647
684
|
chunks.push(`--- ${relPath} (${lines.length} lines) ---\n${body}`);
|
|
648
685
|
}
|
|
@@ -652,6 +689,19 @@ export class ToolExecutor {
|
|
|
652
689
|
}
|
|
653
690
|
return chunks.join('\n\n');
|
|
654
691
|
}
|
|
692
|
+
readArtifact(id, requestedStart, requestedCount) {
|
|
693
|
+
const safeId = String(id ?? '');
|
|
694
|
+
if (!/^artifact-\d+\.txt$/.test(safeId))
|
|
695
|
+
return 'ERROR: invalid artifact id.';
|
|
696
|
+
const file = path.join(this.projectRoot, '.parallel', 'runs', this.agentId, 'artifacts', safeId);
|
|
697
|
+
if (!fs.existsSync(file))
|
|
698
|
+
return `ERROR: artifact not found: ${safeId}`;
|
|
699
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
700
|
+
const start = Math.max(1, Math.floor(Number(requestedStart) || 1));
|
|
701
|
+
const count = Math.min(200, Math.max(1, Math.floor(Number(requestedCount) || 80)));
|
|
702
|
+
const end = Math.min(lines.length, start + count - 1);
|
|
703
|
+
return lines.slice(start - 1, end).map((line, index) => `${String(start + index).padStart(4)}|${line}`).join('\n');
|
|
704
|
+
}
|
|
655
705
|
/**
|
|
656
706
|
* Adaptive co-editing: writing is NEVER blocked by another agent.
|
|
657
707
|
* But if the file changed under you since your last read, you first get
|
|
@@ -730,7 +780,8 @@ export class ToolExecutor {
|
|
|
730
780
|
}
|
|
731
781
|
const results = [];
|
|
732
782
|
const walk = (dir, depth) => {
|
|
733
|
-
|
|
783
|
+
const resultLimit = this.profile === 'quick' ? 40 : this.profile === 'standard' ? 80 : 150;
|
|
784
|
+
if (depth > 6 || results.length >= resultLimit)
|
|
734
785
|
return;
|
|
735
786
|
let entries;
|
|
736
787
|
try {
|
|
@@ -758,11 +809,15 @@ export class ToolExecutor {
|
|
|
758
809
|
continue;
|
|
759
810
|
}
|
|
760
811
|
const lines = content.split('\n');
|
|
761
|
-
|
|
812
|
+
let matched = false;
|
|
813
|
+
for (let i = 0; i < lines.length && results.length < resultLimit; i++) {
|
|
762
814
|
if (re.test(lines[i])) {
|
|
815
|
+
matched = true;
|
|
763
816
|
results.push(`${path.relative(this.projectRoot, full)}:${i + 1}: ${lines[i].trim().slice(0, 160)}`);
|
|
764
817
|
}
|
|
765
818
|
}
|
|
819
|
+
if (matched)
|
|
820
|
+
this.onInspect?.(this.agentId, path.relative(this.projectRoot, full), content);
|
|
766
821
|
}
|
|
767
822
|
}
|
|
768
823
|
};
|
|
@@ -822,12 +877,17 @@ export class ToolExecutor {
|
|
|
822
877
|
return;
|
|
823
878
|
}
|
|
824
879
|
const lines = content.split('\n');
|
|
880
|
+
let matched = false;
|
|
825
881
|
for (let i = 0; i < lines.length && matches.length <= 200; i++) {
|
|
826
882
|
for (const { raw, re } of regexes) {
|
|
827
|
-
if (re.test(lines[i]))
|
|
883
|
+
if (re.test(lines[i])) {
|
|
884
|
+
matched = true;
|
|
828
885
|
matches.push(`${raw} :: ${relPath}:${i + 1}: ${lines[i].trim().slice(0, 140)}`);
|
|
886
|
+
}
|
|
829
887
|
}
|
|
830
888
|
}
|
|
889
|
+
if (matched)
|
|
890
|
+
this.onInspect?.(this.agentId, relPath, content);
|
|
831
891
|
};
|
|
832
892
|
for (const rel of paths)
|
|
833
893
|
visit(this.resolve(rel), 0);
|
|
@@ -849,18 +909,19 @@ export class ToolExecutor {
|
|
|
849
909
|
exec(command, { cwd: this.projectRoot, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
850
910
|
let out = '';
|
|
851
911
|
if (stdout)
|
|
852
|
-
out += stdout;
|
|
912
|
+
out += sanitizeTerminalText(stdout);
|
|
853
913
|
if (stderr)
|
|
854
|
-
out += (out ? '\n--- stderr ---\n' : '') + stderr;
|
|
914
|
+
out += (out ? '\n--- stderr ---\n' : '') + sanitizeTerminalText(stderr);
|
|
855
915
|
if (err && err.killed)
|
|
856
916
|
out += '\n(process killed: 120s timeout)';
|
|
857
917
|
else if (err)
|
|
858
918
|
out += `\n(exit code: ${err.code ?? 1})`;
|
|
859
|
-
if (out.length > MAX_OUTPUT)
|
|
860
|
-
out = out.slice(0, MAX_OUTPUT) + '\n... (output truncated)';
|
|
861
919
|
const result = out || '(no output, success)';
|
|
862
920
|
const changed = this.recordShellMutations(before);
|
|
863
|
-
|
|
921
|
+
const logged = result.length > this.maxOutput
|
|
922
|
+
? `${result.slice(0, this.maxOutput)}\n... (${result.length.toLocaleString()} characters; full output retained as an agent artifact)`
|
|
923
|
+
: result;
|
|
924
|
+
this.board.log(this.agentId, 'tool_result', logged);
|
|
864
925
|
resolve(changed > 0 ? `${result}\n\nTracked shell mutations: ${changed} file${changed === 1 ? '' : 's'}.` : result);
|
|
865
926
|
});
|
|
866
927
|
});
|
package/dist/commands.js
CHANGED
|
@@ -9,9 +9,9 @@ import { t } from './i18n.js';
|
|
|
9
9
|
// inspect the session → git safety net → session & config → exit.
|
|
10
10
|
export const COMMANDS = [
|
|
11
11
|
// create agents
|
|
12
|
-
{ name: '/ask', args: '[Name:] <question> [--model=m]', descKey: 'cmd.ask', group: 'modes', aliases: ['/a'] },
|
|
13
|
-
{ name: '/task', args: '[Name:] <task> [--model=m] [#skill]', descKey: 'cmd.task', group: 'modes', aliases: ['/t'] },
|
|
14
|
-
{ name: '/plan', args: '[Name:] <task> [--model=m]', descKey: 'cmd.plan', group: 'modes', aliases: ['/p'] },
|
|
12
|
+
{ name: '/ask', args: '[Name:] <question> [--quick|--standard|--deep] [--model=m]', descKey: 'cmd.ask', group: 'modes', aliases: ['/a'] },
|
|
13
|
+
{ name: '/task', args: '[Name:] <task> [--quick|--standard|--deep] [--model=m] [#skill]', descKey: 'cmd.task', group: 'modes', aliases: ['/t'] },
|
|
14
|
+
{ name: '/plan', args: '[Name:] <task> [--quick|--standard|--deep] [--model=m]', descKey: 'cmd.plan', group: 'modes', aliases: ['/p'] },
|
|
15
15
|
{ name: '/review', args: '[agent|all] [prompt]', descKey: 'cmd.review', group: 'modes' },
|
|
16
16
|
{ name: '/issue', args: '<n>', descKey: 'cmd.issue', group: 'git' },
|
|
17
17
|
{ name: '/specialist', args: '<name> <task> | new <name> [global]', descKey: 'cmd.specialist', group: 'modes' },
|
|
@@ -39,6 +39,7 @@ export const COMMANDS = [
|
|
|
39
39
|
{ name: '/diff', args: '', descKey: 'cmd.diff', group: 'views' },
|
|
40
40
|
{ name: '/cost', args: '', descKey: 'cmd.cost', group: 'views' },
|
|
41
41
|
{ name: '/status', args: '', descKey: 'cmd.status', group: 'views' },
|
|
42
|
+
{ name: '/memory', args: '[refresh]', descKey: 'cmd.memory', group: 'views' },
|
|
42
43
|
// sessions
|
|
43
44
|
{ name: '/save', args: '[name]', descKey: 'cmd.save', group: 'git' },
|
|
44
45
|
{ name: '/sessions', args: '', descKey: 'cmd.sessions', group: 'git' },
|
|
@@ -165,8 +166,17 @@ async function doctorReport(ctl, ui) {
|
|
|
165
166
|
}
|
|
166
167
|
const sock = path.join(ctl.projectRoot, '.parallel', 'session.sock');
|
|
167
168
|
lines.push(fs.existsSync(sock) ? t('m.doctorAttachOk') : t('m.doctorAttachMissing'));
|
|
169
|
+
for (const line of ctl.securityDiagnostics()) {
|
|
170
|
+
if (line.startsWith('warn') && level !== 'error')
|
|
171
|
+
level = 'warn';
|
|
172
|
+
lines.push(`security ${line}`);
|
|
173
|
+
}
|
|
168
174
|
lines.push(commandExists('git') ? t('m.doctorGitOk') : t('m.doctorGitMissing'));
|
|
169
175
|
lines.push(commandExists('gh') ? t('m.doctorGhOk') : t('m.doctorGhMissing'));
|
|
176
|
+
const memory = ctl.projectContextStatus();
|
|
177
|
+
if (['idle', 'fallback', 'error'].includes(memory.status) && level !== 'error')
|
|
178
|
+
level = 'warn';
|
|
179
|
+
lines.push(t('m.doctorMemory', { status: memory.status, date: memory.generatedAt ?? '—' }));
|
|
170
180
|
ui.system(t('m.doctorReport', { lines: lines.join('\n') }), level);
|
|
171
181
|
}
|
|
172
182
|
/**
|
|
@@ -220,12 +230,21 @@ function spawnFrom(arg, ctl, ui, images, specialist, mode = 'task') {
|
|
|
220
230
|
}
|
|
221
231
|
// optional --model=xxx flag
|
|
222
232
|
let model;
|
|
233
|
+
let profile;
|
|
223
234
|
let task = arg;
|
|
224
235
|
const mFlag = task.match(/\s--model=(\S+)/);
|
|
225
236
|
if (mFlag) {
|
|
226
237
|
model = mFlag[1];
|
|
227
238
|
task = task.replace(mFlag[0], '').trim();
|
|
228
239
|
}
|
|
240
|
+
for (const candidate of ['quick', 'standard', 'deep']) {
|
|
241
|
+
const flag = `--${candidate}`;
|
|
242
|
+
if (new RegExp(`(^|\\s)${flag}(?=\\s|$)`).test(task)) {
|
|
243
|
+
profile = candidate;
|
|
244
|
+
task = task.replace(new RegExp(`(^|\\s)${flag}(?=\\s|$)`), ' ').trim();
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
229
248
|
// optional #skill tokens → force-load these skills at the start of the task
|
|
230
249
|
const forced = [];
|
|
231
250
|
const available = ctl.getSkills();
|
|
@@ -244,11 +263,12 @@ function spawnFrom(arg, ctl, ui, images, specialist, mode = 'task') {
|
|
|
244
263
|
// optional "Name:" prefix
|
|
245
264
|
const named = task.match(/^([\p{L}\p{N}_-]{1,16}):\s+(.+)$/su);
|
|
246
265
|
const finalTask = named ? named[2] : task;
|
|
247
|
-
const agent = ctl.spawnAgent(finalTask, named ? named[1] : undefined, model, images, specialist, undefined, mode);
|
|
266
|
+
const agent = ctl.spawnAgent(finalTask, named ? named[1] : undefined, model, images, specialist, undefined, mode, profile);
|
|
248
267
|
if (!agent)
|
|
249
268
|
return ui.system(specialist ? t('m.noSpecialist', { name: specialist }) : t('m.spawnFail'), 'error');
|
|
250
269
|
ui.system(t('m.spawned', { name: agent.name, model: model ? ` (${model})` : '' }) +
|
|
251
270
|
` /${mode}` +
|
|
271
|
+
` [${ctl.board.agents.get(agent.id)?.profile ?? profile ?? 'auto'}]` +
|
|
252
272
|
(specialist ? ` 🎓${specialist}` : '') +
|
|
253
273
|
(forced.length > 0 ? ` 🧩${forced.join(',')}` : ''), 'info');
|
|
254
274
|
}
|
|
@@ -490,6 +510,34 @@ export function executeInput(raw, ctl, ui, images) {
|
|
|
490
510
|
case '/cost':
|
|
491
511
|
ui.setView('cost');
|
|
492
512
|
return;
|
|
513
|
+
case '/memory': {
|
|
514
|
+
if (arg && arg !== 'refresh')
|
|
515
|
+
return ui.system(t('m.usageMemory'), 'warn');
|
|
516
|
+
if (arg === 'refresh') {
|
|
517
|
+
ui.system(t('memory.indexing'), 'info');
|
|
518
|
+
void ctl.refreshProjectContext().then(() => {
|
|
519
|
+
const memory = ctl.projectContextStatus();
|
|
520
|
+
ui.system(t('m.memoryStatus', {
|
|
521
|
+
status: memory.status,
|
|
522
|
+
date: memory.generatedAt ?? '—',
|
|
523
|
+
model: memory.model ?? '—',
|
|
524
|
+
tokens: memory.tokensIn + memory.tokensOut,
|
|
525
|
+
cost: memory.cost === null ? '—' : memory.cost.toFixed(4),
|
|
526
|
+
}), memory.status === 'ready' ? 'ok' : 'warn');
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const memory = ctl.projectContextStatus();
|
|
531
|
+
const index = ctl.projectIndexStatus();
|
|
532
|
+
ui.system(t('m.memoryStatus', {
|
|
533
|
+
status: memory.status,
|
|
534
|
+
date: memory.generatedAt ?? '—',
|
|
535
|
+
model: memory.model ?? '—',
|
|
536
|
+
tokens: memory.tokensIn + memory.tokensOut,
|
|
537
|
+
cost: memory.cost === null ? '—' : memory.cost.toFixed(4),
|
|
538
|
+
}) + ` · index ${index.files} files/${index.symbols} symbols`, memory.status === 'ready' ? 'ok' : memory.status === 'indexing' ? 'info' : 'warn');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
493
541
|
case '/status': {
|
|
494
542
|
const p = ctl.sessionProvider();
|
|
495
543
|
const agents = [...ctl.board.agents.values()];
|
|
@@ -497,8 +545,22 @@ export function executeInput(raw, ctl, ui, images) {
|
|
|
497
545
|
const cost = agents.reduce((s, a) => s + (a.cost ?? 0), 0);
|
|
498
546
|
const changed = new Set(ctl.board.changes.map((c) => c.path)).size;
|
|
499
547
|
const pm = p ? `${p.name}:${ctl.session.model}` : '-';
|
|
548
|
+
const memory = ctl.projectContextStatus();
|
|
549
|
+
const profiles = agents.reduce((counts, agent) => {
|
|
550
|
+
counts[agent.profile] = (counts[agent.profile] ?? 0) + 1;
|
|
551
|
+
return counts;
|
|
552
|
+
}, {});
|
|
500
553
|
// Multiline: each metric on its own line for readability.
|
|
501
|
-
ui.system(t('m.status', {
|
|
554
|
+
ui.system(t('m.status', {
|
|
555
|
+
pm,
|
|
556
|
+
approval: ctl.session.approvalMode,
|
|
557
|
+
total: agents.length,
|
|
558
|
+
active,
|
|
559
|
+
changed,
|
|
560
|
+
cost: cost.toFixed(3),
|
|
561
|
+
memory: memory.status,
|
|
562
|
+
memoryCost: memory.cost === null ? '—' : memory.cost.toFixed(4),
|
|
563
|
+
}) + `\nProfiles: ${Object.entries(profiles).map(([profile, count]) => `${profile}=${count}`).join(', ') || 'none'}`, 'info');
|
|
502
564
|
return;
|
|
503
565
|
}
|
|
504
566
|
case '/raw':
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
+
import { chmodPrivateTree, ensurePrivateDir, writeJsonAtomicPrivate } from './security.js';
|
|
4
5
|
let configHomeOverride;
|
|
5
6
|
export function setConfigHome(dir) {
|
|
6
7
|
configHomeOverride = path.resolve(dir.replace(/^~(?=$|\/)/, os.homedir()));
|
|
@@ -360,6 +361,8 @@ function normalizeConfig(cfg) {
|
|
|
360
361
|
export function loadConfig() {
|
|
361
362
|
let cfg = { ...DEFAULTS, providers: [] };
|
|
362
363
|
try {
|
|
364
|
+
ensurePrivateDir(configDir());
|
|
365
|
+
chmodPrivateTree(configDir());
|
|
363
366
|
const file = configFile();
|
|
364
367
|
if (fs.existsSync(file)) {
|
|
365
368
|
const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -414,8 +417,8 @@ export function loadConfig() {
|
|
|
414
417
|
}
|
|
415
418
|
export function saveConfig(cfg) {
|
|
416
419
|
try {
|
|
417
|
-
|
|
418
|
-
|
|
420
|
+
ensurePrivateDir(configDir());
|
|
421
|
+
writeJsonAtomicPrivate(configFile(), cfg);
|
|
419
422
|
}
|
|
420
423
|
catch {
|
|
421
424
|
// best effort
|