pi-crew 0.1.37 → 0.1.39
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/AGENTS.md +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +5 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/resource-formats.md +10 -8
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +6 -0
- package/index.ts +6 -6
- package/package.json +3 -3
- package/schema.json +2 -2
- package/src/agents/agent-serializer.ts +34 -34
- package/src/config/config.ts +8 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +18 -2
- package/src/extension/register.ts +11 -1
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +30 -6
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +12 -2
- package/src/extension/run-index.ts +12 -2
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +54 -14
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -179
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/status.ts +73 -73
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +54 -54
- package/src/observability/exporters/adapter.ts +24 -24
- package/src/observability/exporters/otlp-exporter.ts +65 -65
- package/src/observability/exporters/prometheus-exporter.ts +47 -47
- package/src/observability/metric-registry.ts +72 -72
- package/src/observability/metric-retention.ts +46 -46
- package/src/observability/metric-sink.ts +51 -51
- package/src/observability/metrics-primitives.ts +166 -166
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crash-recovery.ts +56 -56
- package/src/runtime/crew-agent-records.ts +54 -9
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +36 -36
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +88 -88
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +80 -80
- package/src/runtime/live-agent-control.ts +87 -78
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +299 -299
- package/src/runtime/manifest-cache.ts +248 -212
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +59 -59
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +80 -12
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -106
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +1 -1
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +21 -21
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +122 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/mailbox.ts +67 -22
- package/src/state/state-store.ts +36 -5
- package/src/state/task-claims.ts +42 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +27 -5
- package/src/teams/team-serializer.ts +38 -36
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +5 -2
- package/src/ui/run-snapshot-cache.ts +19 -8
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +15 -1
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +3 -2
- package/src/utils/safe-paths.ts +34 -0
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +30 -3
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
import { supportsLanguage, highlight } from "cli-highlight";
|
|
2
|
-
import type { CrewTheme } from "./theme-adapter.ts";
|
|
3
|
-
import { asCrewTheme } from "./theme-adapter.ts";
|
|
4
|
-
|
|
5
|
-
function buildCliTheme(theme: CrewTheme): Record<string, (text: string) => string> {
|
|
6
|
-
return {
|
|
7
|
-
keyword: (text) => theme.fg("syntaxKeyword", text),
|
|
8
|
-
built_in: (text) => theme.fg("syntaxType", text),
|
|
9
|
-
literal: (text) => theme.fg("syntaxNumber", text),
|
|
10
|
-
number: (text) => theme.fg("syntaxNumber", text),
|
|
11
|
-
string: (text) => theme.fg("syntaxString", text),
|
|
12
|
-
comment: (text) => theme.fg("syntaxComment", text),
|
|
13
|
-
function: (text) => theme.fg("syntaxFunction", text),
|
|
14
|
-
title: (text) => theme.fg("syntaxFunction", text),
|
|
15
|
-
class: (text) => theme.fg("syntaxType", text),
|
|
16
|
-
type: (text) => theme.fg("syntaxType", text),
|
|
17
|
-
attr: (text) => theme.fg("syntaxVariable", text),
|
|
18
|
-
variable: (text) => theme.fg("syntaxVariable", text),
|
|
19
|
-
params: (text) => theme.fg("syntaxVariable", text),
|
|
20
|
-
operator: (text) => theme.fg("syntaxOperator", text),
|
|
21
|
-
punctuation: (text) => theme.fg("syntaxPunctuation", text),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function detectLanguageFromPath(filePath: string): string | undefined {
|
|
26
|
-
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
27
|
-
if (!ext) return undefined;
|
|
28
|
-
return languageMap[ext];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const languageMap: Record<string, string> = {
|
|
32
|
-
ts: "typescript",
|
|
33
|
-
tsx: "typescript",
|
|
34
|
-
js: "javascript",
|
|
35
|
-
jsx: "javascript",
|
|
36
|
-
mjs: "javascript",
|
|
37
|
-
cjs: "javascript",
|
|
38
|
-
py: "python",
|
|
39
|
-
md: "markdown",
|
|
40
|
-
markdown: "markdown",
|
|
41
|
-
json: "json",
|
|
42
|
-
yml: "yaml",
|
|
43
|
-
yaml: "yaml",
|
|
44
|
-
toml: "yaml",
|
|
45
|
-
html: "html",
|
|
46
|
-
htm: "html",
|
|
47
|
-
css: "css",
|
|
48
|
-
scss: "scss",
|
|
49
|
-
sass: "sass",
|
|
50
|
-
bash: "bash",
|
|
51
|
-
sh: "bash",
|
|
52
|
-
zsh: "bash",
|
|
53
|
-
fish: "bash",
|
|
54
|
-
ps1: "powershell",
|
|
55
|
-
sql: "sql",
|
|
56
|
-
rust: "rust",
|
|
57
|
-
rb: "ruby",
|
|
58
|
-
go: "go",
|
|
59
|
-
java: "java",
|
|
60
|
-
kt: "kotlin",
|
|
61
|
-
cpp: "cpp",
|
|
62
|
-
cc: "cpp",
|
|
63
|
-
cxx: "cpp",
|
|
64
|
-
hpp: "cpp",
|
|
65
|
-
c: "c",
|
|
66
|
-
h: "c",
|
|
67
|
-
cs: "csharp",
|
|
68
|
-
php: "php",
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
export function highlightCode(code: string, language: string | undefined, themeLike: unknown = undefined): string {
|
|
72
|
-
const theme = asCrewTheme(themeLike);
|
|
73
|
-
const validLanguage = language && supportsLanguage(language) ? language : undefined;
|
|
74
|
-
if (!validLanguage) {
|
|
75
|
-
return code
|
|
76
|
-
.split("\n")
|
|
77
|
-
.map((line) => theme.fg("mdCodeBlock", line))
|
|
78
|
-
.join("\n");
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
return highlight(code, {
|
|
82
|
-
language: validLanguage,
|
|
83
|
-
ignoreIllegals: true,
|
|
84
|
-
theme: buildCliTheme(theme),
|
|
85
|
-
}).trimEnd();
|
|
86
|
-
} catch {
|
|
87
|
-
return code
|
|
88
|
-
.split("\n")
|
|
89
|
-
.map((line) => theme.fg("mdCodeBlock", line))
|
|
90
|
-
.join("\n");
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function highlightJson(payload: string, themeLike: unknown = undefined): string {
|
|
95
|
-
const theme = asCrewTheme(themeLike);
|
|
96
|
-
try {
|
|
97
|
-
return highlight(payload, {
|
|
98
|
-
language: "json",
|
|
99
|
-
ignoreIllegals: true,
|
|
100
|
-
theme: buildCliTheme(theme),
|
|
101
|
-
}).trimEnd();
|
|
102
|
-
} catch {
|
|
103
|
-
try {
|
|
104
|
-
const parsed = JSON.parse(payload);
|
|
105
|
-
return JSON.stringify(parsed, null, 2)
|
|
106
|
-
.split("\n")
|
|
107
|
-
.map((line) => theme.fg("mdCodeBlock", line))
|
|
108
|
-
.join("\n");
|
|
109
|
-
} catch {
|
|
110
|
-
return payload
|
|
111
|
-
.split("\n")
|
|
112
|
-
.map((line) => theme.fg("mdCodeBlock", line))
|
|
113
|
-
.join("\n");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
1
|
+
import { supportsLanguage, highlight } from "cli-highlight";
|
|
2
|
+
import type { CrewTheme } from "./theme-adapter.ts";
|
|
3
|
+
import { asCrewTheme } from "./theme-adapter.ts";
|
|
4
|
+
|
|
5
|
+
function buildCliTheme(theme: CrewTheme): Record<string, (text: string) => string> {
|
|
6
|
+
return {
|
|
7
|
+
keyword: (text) => theme.fg("syntaxKeyword", text),
|
|
8
|
+
built_in: (text) => theme.fg("syntaxType", text),
|
|
9
|
+
literal: (text) => theme.fg("syntaxNumber", text),
|
|
10
|
+
number: (text) => theme.fg("syntaxNumber", text),
|
|
11
|
+
string: (text) => theme.fg("syntaxString", text),
|
|
12
|
+
comment: (text) => theme.fg("syntaxComment", text),
|
|
13
|
+
function: (text) => theme.fg("syntaxFunction", text),
|
|
14
|
+
title: (text) => theme.fg("syntaxFunction", text),
|
|
15
|
+
class: (text) => theme.fg("syntaxType", text),
|
|
16
|
+
type: (text) => theme.fg("syntaxType", text),
|
|
17
|
+
attr: (text) => theme.fg("syntaxVariable", text),
|
|
18
|
+
variable: (text) => theme.fg("syntaxVariable", text),
|
|
19
|
+
params: (text) => theme.fg("syntaxVariable", text),
|
|
20
|
+
operator: (text) => theme.fg("syntaxOperator", text),
|
|
21
|
+
punctuation: (text) => theme.fg("syntaxPunctuation", text),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function detectLanguageFromPath(filePath: string): string | undefined {
|
|
26
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
27
|
+
if (!ext) return undefined;
|
|
28
|
+
return languageMap[ext];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const languageMap: Record<string, string> = {
|
|
32
|
+
ts: "typescript",
|
|
33
|
+
tsx: "typescript",
|
|
34
|
+
js: "javascript",
|
|
35
|
+
jsx: "javascript",
|
|
36
|
+
mjs: "javascript",
|
|
37
|
+
cjs: "javascript",
|
|
38
|
+
py: "python",
|
|
39
|
+
md: "markdown",
|
|
40
|
+
markdown: "markdown",
|
|
41
|
+
json: "json",
|
|
42
|
+
yml: "yaml",
|
|
43
|
+
yaml: "yaml",
|
|
44
|
+
toml: "yaml",
|
|
45
|
+
html: "html",
|
|
46
|
+
htm: "html",
|
|
47
|
+
css: "css",
|
|
48
|
+
scss: "scss",
|
|
49
|
+
sass: "sass",
|
|
50
|
+
bash: "bash",
|
|
51
|
+
sh: "bash",
|
|
52
|
+
zsh: "bash",
|
|
53
|
+
fish: "bash",
|
|
54
|
+
ps1: "powershell",
|
|
55
|
+
sql: "sql",
|
|
56
|
+
rust: "rust",
|
|
57
|
+
rb: "ruby",
|
|
58
|
+
go: "go",
|
|
59
|
+
java: "java",
|
|
60
|
+
kt: "kotlin",
|
|
61
|
+
cpp: "cpp",
|
|
62
|
+
cc: "cpp",
|
|
63
|
+
cxx: "cpp",
|
|
64
|
+
hpp: "cpp",
|
|
65
|
+
c: "c",
|
|
66
|
+
h: "c",
|
|
67
|
+
cs: "csharp",
|
|
68
|
+
php: "php",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function highlightCode(code: string, language: string | undefined, themeLike: unknown = undefined): string {
|
|
72
|
+
const theme = asCrewTheme(themeLike);
|
|
73
|
+
const validLanguage = language && supportsLanguage(language) ? language : undefined;
|
|
74
|
+
if (!validLanguage) {
|
|
75
|
+
return code
|
|
76
|
+
.split("\n")
|
|
77
|
+
.map((line) => theme.fg("mdCodeBlock", line))
|
|
78
|
+
.join("\n");
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return highlight(code, {
|
|
82
|
+
language: validLanguage,
|
|
83
|
+
ignoreIllegals: true,
|
|
84
|
+
theme: buildCliTheme(theme),
|
|
85
|
+
}).trimEnd();
|
|
86
|
+
} catch {
|
|
87
|
+
return code
|
|
88
|
+
.split("\n")
|
|
89
|
+
.map((line) => theme.fg("mdCodeBlock", line))
|
|
90
|
+
.join("\n");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function highlightJson(payload: string, themeLike: unknown = undefined): string {
|
|
95
|
+
const theme = asCrewTheme(themeLike);
|
|
96
|
+
try {
|
|
97
|
+
return highlight(payload, {
|
|
98
|
+
language: "json",
|
|
99
|
+
ignoreIllegals: true,
|
|
100
|
+
theme: buildCliTheme(theme),
|
|
101
|
+
}).trimEnd();
|
|
102
|
+
} catch {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(payload);
|
|
105
|
+
return JSON.stringify(parsed, null, 2)
|
|
106
|
+
.split("\n")
|
|
107
|
+
.map((line) => theme.fg("mdCodeBlock", line))
|
|
108
|
+
.join("\n");
|
|
109
|
+
} catch {
|
|
110
|
+
return payload
|
|
111
|
+
.split("\n")
|
|
112
|
+
.map((line) => theme.fg("mdCodeBlock", line))
|
|
113
|
+
.join("\n");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -8,6 +8,7 @@ import { highlightCode, highlightJson } from "./syntax-highlight.ts";
|
|
|
8
8
|
import { pad, truncate, truncateToVisualLines } from "../utils/visual.ts";
|
|
9
9
|
import { colorForStatus, iconForStatus, type RunStatus } from "./status-colors.ts";
|
|
10
10
|
import { DEFAULT_TRANSCRIPT_TAIL_BYTES, getTranscriptCacheEntry, readTranscriptLinesCached } from "./transcript-cache.ts";
|
|
11
|
+
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
11
12
|
|
|
12
13
|
type Component = { invalidate(): void; render(width: number): string[]; handleInput(data: string): void };
|
|
13
14
|
|
|
@@ -128,7 +129,20 @@ export function readRunTranscript(manifest: TeamRunManifest, taskId?: string, op
|
|
|
128
129
|
const agents = readCrewAgents(manifest);
|
|
129
130
|
const agent = taskId ? agents.find((item) => item.taskId === taskId || item.id === taskId) : agents.find((item) => item.transcriptPath) ?? agents[0];
|
|
130
131
|
const selectedTaskId = agent?.taskId ?? taskId ?? "unknown";
|
|
131
|
-
|
|
132
|
+
let transcriptPath: string;
|
|
133
|
+
try {
|
|
134
|
+
transcriptPath = agentOutputPath(manifest, selectedTaskId);
|
|
135
|
+
} catch {
|
|
136
|
+
transcriptPath = agentOutputPath(manifest, "unknown");
|
|
137
|
+
}
|
|
138
|
+
if (agent?.transcriptPath) {
|
|
139
|
+
try {
|
|
140
|
+
const safeTranscriptPath = resolveRealContainedPath(manifest.artifactsRoot, agent.transcriptPath);
|
|
141
|
+
if (fs.existsSync(safeTranscriptPath)) transcriptPath = safeTranscriptPath;
|
|
142
|
+
} catch {
|
|
143
|
+
// Ignore untrusted transcript paths from mutable agent state and fall back to durable agent output.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
132
146
|
const readOptions = { full: options.full === true, maxTailBytes: options.maxTailBytes ?? DEFAULT_TRANSCRIPT_TAIL_BYTES };
|
|
133
147
|
const lines = readTranscriptLinesCached(transcriptPath, (text) => formatTranscriptText(text), Date.now(), readOptions);
|
|
134
148
|
const entry = getTranscriptCacheEntry(transcriptPath, readOptions);
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
interface CompletionDataLike {
|
|
2
|
-
id?: unknown;
|
|
3
|
-
agent?: unknown;
|
|
4
|
-
timestamp?: unknown;
|
|
5
|
-
sessionId?: unknown;
|
|
6
|
-
taskIndex?: unknown;
|
|
7
|
-
totalTasks?: unknown;
|
|
8
|
-
success?: unknown;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function asNonEmptyString(value: unknown): string | undefined {
|
|
12
|
-
if (typeof value !== "string") return undefined;
|
|
13
|
-
const trimmed = value.trim();
|
|
14
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function asFiniteNumber(value: unknown): number | undefined {
|
|
18
|
-
if (typeof value !== "number") return undefined;
|
|
19
|
-
return Number.isFinite(value) ? value : undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
|
|
23
|
-
const id = asNonEmptyString(data.id);
|
|
24
|
-
if (id) return `id:${id}`;
|
|
25
|
-
const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
|
|
26
|
-
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
27
|
-
const timestamp = asFiniteNumber(data.timestamp);
|
|
28
|
-
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
29
|
-
const totalTasks = asFiniteNumber(data.totalTasks);
|
|
30
|
-
const success = typeof data.success === "boolean" ? (data.success ? "1" : "0") : "?";
|
|
31
|
-
return [
|
|
32
|
-
"meta",
|
|
33
|
-
sessionId,
|
|
34
|
-
agent,
|
|
35
|
-
timestamp !== undefined ? String(timestamp) : "no-ts",
|
|
36
|
-
taskIndex !== undefined ? String(taskIndex) : "-",
|
|
37
|
-
totalTasks !== undefined ? String(totalTasks) : "-",
|
|
38
|
-
success,
|
|
39
|
-
fallback,
|
|
40
|
-
].join(":");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function pruneSeenMap(seen: Map<string, number>, now: number, ttlMs: number): void {
|
|
44
|
-
for (const [key, ts] of seen.entries()) {
|
|
45
|
-
if (now - ts > ttlMs) seen.delete(key);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function markSeenWithTtl(seen: Map<string, number>, key: string, now: number, ttlMs: number): boolean {
|
|
50
|
-
pruneSeenMap(seen, now, ttlMs);
|
|
51
|
-
if (seen.has(key)) return true;
|
|
52
|
-
seen.set(key, now);
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function getGlobalSeenMap(storeKey: string): Map<string, number> {
|
|
57
|
-
const globalStore = globalThis as Record<string, unknown>;
|
|
58
|
-
const existing = globalStore[storeKey];
|
|
59
|
-
if (existing instanceof Map) return existing as Map<string, number>;
|
|
60
|
-
const map = new Map<string, number>();
|
|
61
|
-
globalStore[storeKey] = map;
|
|
62
|
-
return map;
|
|
63
|
-
}
|
|
1
|
+
interface CompletionDataLike {
|
|
2
|
+
id?: unknown;
|
|
3
|
+
agent?: unknown;
|
|
4
|
+
timestamp?: unknown;
|
|
5
|
+
sessionId?: unknown;
|
|
6
|
+
taskIndex?: unknown;
|
|
7
|
+
totalTasks?: unknown;
|
|
8
|
+
success?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function asNonEmptyString(value: unknown): string | undefined {
|
|
12
|
+
if (typeof value !== "string") return undefined;
|
|
13
|
+
const trimmed = value.trim();
|
|
14
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function asFiniteNumber(value: unknown): number | undefined {
|
|
18
|
+
if (typeof value !== "number") return undefined;
|
|
19
|
+
return Number.isFinite(value) ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
|
|
23
|
+
const id = asNonEmptyString(data.id);
|
|
24
|
+
if (id) return `id:${id}`;
|
|
25
|
+
const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
|
|
26
|
+
const agent = asNonEmptyString(data.agent) ?? "unknown";
|
|
27
|
+
const timestamp = asFiniteNumber(data.timestamp);
|
|
28
|
+
const taskIndex = asFiniteNumber(data.taskIndex);
|
|
29
|
+
const totalTasks = asFiniteNumber(data.totalTasks);
|
|
30
|
+
const success = typeof data.success === "boolean" ? (data.success ? "1" : "0") : "?";
|
|
31
|
+
return [
|
|
32
|
+
"meta",
|
|
33
|
+
sessionId,
|
|
34
|
+
agent,
|
|
35
|
+
timestamp !== undefined ? String(timestamp) : "no-ts",
|
|
36
|
+
taskIndex !== undefined ? String(taskIndex) : "-",
|
|
37
|
+
totalTasks !== undefined ? String(totalTasks) : "-",
|
|
38
|
+
success,
|
|
39
|
+
fallback,
|
|
40
|
+
].join(":");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function pruneSeenMap(seen: Map<string, number>, now: number, ttlMs: number): void {
|
|
44
|
+
for (const [key, ts] of seen.entries()) {
|
|
45
|
+
if (now - ts > ttlMs) seen.delete(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function markSeenWithTtl(seen: Map<string, number>, key: string, now: number, ttlMs: number): boolean {
|
|
50
|
+
pruneSeenMap(seen, now, ttlMs);
|
|
51
|
+
if (seen.has(key)) return true;
|
|
52
|
+
seen.set(key, now);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getGlobalSeenMap(storeKey: string): Map<string, number> {
|
|
57
|
+
const globalStore = globalThis as Record<string, unknown>;
|
|
58
|
+
const existing = globalStore[storeKey];
|
|
59
|
+
if (existing instanceof Map) return existing as Map<string, number>;
|
|
60
|
+
const map = new Map<string, number>();
|
|
61
|
+
globalStore[storeKey] = map;
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
|
|
3
|
-
interface TimerApi {
|
|
4
|
-
setTimeout(handler: () => void, delayMs: number): unknown;
|
|
5
|
-
clearTimeout(handle: unknown): void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const defaultTimerApi: TimerApi = {
|
|
9
|
-
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
10
|
-
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export interface FileCoalescer {
|
|
14
|
-
schedule(file: string, delayMs?: number): boolean;
|
|
15
|
-
clear(): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function createFileCoalescer(handler: (file: string) => void, defaultDelayMs: number, timerApi: TimerApi = defaultTimerApi): FileCoalescer {
|
|
19
|
-
const pending = new Map<string, unknown>();
|
|
20
|
-
return {
|
|
21
|
-
schedule(file, delayMs = defaultDelayMs) {
|
|
22
|
-
if (pending.has(file)) return false;
|
|
23
|
-
const timer = timerApi.setTimeout(() => {
|
|
24
|
-
pending.delete(file);
|
|
25
|
-
handler(file);
|
|
26
|
-
}, delayMs);
|
|
27
|
-
pending.set(file, timer);
|
|
28
|
-
return true;
|
|
29
|
-
},
|
|
30
|
-
clear() {
|
|
31
|
-
for (const timer of pending.values()) timerApi.clearTimeout(timer);
|
|
32
|
-
pending.clear();
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ReadCacheEntry<T> {
|
|
38
|
-
value: T;
|
|
39
|
-
mtimeMs: number;
|
|
40
|
-
size: number;
|
|
41
|
-
expiresAt: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const readCache = new Map<string, ReadCacheEntry<unknown>>();
|
|
45
|
-
const readCacheSizeLimit = 128;
|
|
46
|
-
|
|
47
|
-
function evictOldestCacheEntry(): void {
|
|
48
|
-
if (readCache.size < readCacheSizeLimit) return;
|
|
49
|
-
const oldestKey = readCache.keys().next().value;
|
|
50
|
-
if (oldestKey !== undefined) {
|
|
51
|
-
readCache.delete(oldestKey);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function clearReadCache(): void {
|
|
56
|
-
readCache.clear();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function readJsonFileCoalesced<T>(filePath: string, ttlMs: number, read: () => T): T {
|
|
60
|
-
const now = Date.now();
|
|
61
|
-
const stat = (() => {
|
|
62
|
-
try {
|
|
63
|
-
const fileStat = fs.statSync(filePath);
|
|
64
|
-
return { mtimeMs: fileStat.mtimeMs, size: fileStat.size };
|
|
65
|
-
} catch {
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
})();
|
|
69
|
-
const cached = readCache.get(filePath);
|
|
70
|
-
if (cached && stat && cached.expiresAt > now && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
71
|
-
return cached.value as T;
|
|
72
|
-
}
|
|
73
|
-
const value = read();
|
|
74
|
-
if (stat !== undefined) {
|
|
75
|
-
readCache.set(filePath, {
|
|
76
|
-
value,
|
|
77
|
-
mtimeMs: stat.mtimeMs,
|
|
78
|
-
size: stat.size,
|
|
79
|
-
expiresAt: now + ttlMs,
|
|
80
|
-
});
|
|
81
|
-
evictOldestCacheEntry();
|
|
82
|
-
}
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
interface TimerApi {
|
|
4
|
+
setTimeout(handler: () => void, delayMs: number): unknown;
|
|
5
|
+
clearTimeout(handle: unknown): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const defaultTimerApi: TimerApi = {
|
|
9
|
+
setTimeout: (handler, delayMs) => setTimeout(handler, delayMs),
|
|
10
|
+
clearTimeout: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface FileCoalescer {
|
|
14
|
+
schedule(file: string, delayMs?: number): boolean;
|
|
15
|
+
clear(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createFileCoalescer(handler: (file: string) => void, defaultDelayMs: number, timerApi: TimerApi = defaultTimerApi): FileCoalescer {
|
|
19
|
+
const pending = new Map<string, unknown>();
|
|
20
|
+
return {
|
|
21
|
+
schedule(file, delayMs = defaultDelayMs) {
|
|
22
|
+
if (pending.has(file)) return false;
|
|
23
|
+
const timer = timerApi.setTimeout(() => {
|
|
24
|
+
pending.delete(file);
|
|
25
|
+
handler(file);
|
|
26
|
+
}, delayMs);
|
|
27
|
+
pending.set(file, timer);
|
|
28
|
+
return true;
|
|
29
|
+
},
|
|
30
|
+
clear() {
|
|
31
|
+
for (const timer of pending.values()) timerApi.clearTimeout(timer);
|
|
32
|
+
pending.clear();
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ReadCacheEntry<T> {
|
|
38
|
+
value: T;
|
|
39
|
+
mtimeMs: number;
|
|
40
|
+
size: number;
|
|
41
|
+
expiresAt: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const readCache = new Map<string, ReadCacheEntry<unknown>>();
|
|
45
|
+
const readCacheSizeLimit = 128;
|
|
46
|
+
|
|
47
|
+
function evictOldestCacheEntry(): void {
|
|
48
|
+
if (readCache.size < readCacheSizeLimit) return;
|
|
49
|
+
const oldestKey = readCache.keys().next().value;
|
|
50
|
+
if (oldestKey !== undefined) {
|
|
51
|
+
readCache.delete(oldestKey);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function clearReadCache(): void {
|
|
56
|
+
readCache.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readJsonFileCoalesced<T>(filePath: string, ttlMs: number, read: () => T): T {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const stat = (() => {
|
|
62
|
+
try {
|
|
63
|
+
const fileStat = fs.statSync(filePath);
|
|
64
|
+
return { mtimeMs: fileStat.mtimeMs, size: fileStat.size };
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
69
|
+
const cached = readCache.get(filePath);
|
|
70
|
+
if (cached && stat && cached.expiresAt > now && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
71
|
+
return cached.value as T;
|
|
72
|
+
}
|
|
73
|
+
const value = read();
|
|
74
|
+
if (stat !== undefined) {
|
|
75
|
+
readCache.set(filePath, {
|
|
76
|
+
value,
|
|
77
|
+
mtimeMs: stat.mtimeMs,
|
|
78
|
+
size: stat.size,
|
|
79
|
+
expiresAt: now + ttlMs,
|
|
80
|
+
});
|
|
81
|
+
evictOldestCacheEntry();
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
package/src/utils/frontmatter.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
export interface ParsedFrontmatter {
|
|
2
|
-
frontmatter: Record<string, string>;
|
|
3
|
-
body: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function parseFrontmatter(content: string): ParsedFrontmatter {
|
|
7
|
-
if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
|
|
8
|
-
return { frontmatter: {}, body: content };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const normalized = content.replaceAll("\r\n", "\n");
|
|
12
|
-
const end = normalized.indexOf("\n---\n", 4);
|
|
13
|
-
if (end === -1) return { frontmatter: {}, body: content };
|
|
14
|
-
|
|
15
|
-
const raw = normalized.slice(4, end);
|
|
16
|
-
const body = normalized.slice(end + "\n---\n".length);
|
|
17
|
-
const frontmatter: Record<string, string> = {};
|
|
18
|
-
|
|
19
|
-
for (const line of raw.split("\n")) {
|
|
20
|
-
const trimmed = line.trim();
|
|
21
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
22
|
-
const separator = trimmed.indexOf(":");
|
|
23
|
-
if (separator === -1) continue;
|
|
24
|
-
const key = trimmed.slice(0, separator).trim();
|
|
25
|
-
const value = trimmed.slice(separator + 1).trim();
|
|
26
|
-
if (key) frontmatter[key] = value;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return { frontmatter, body };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function parseCsv(value: string | undefined): string[] | undefined {
|
|
33
|
-
if (value === undefined) return undefined;
|
|
34
|
-
const values = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
35
|
-
return values.length > 0 ? [...new Set(values)] : undefined;
|
|
36
|
-
}
|
|
1
|
+
export interface ParsedFrontmatter {
|
|
2
|
+
frontmatter: Record<string, string>;
|
|
3
|
+
body: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function parseFrontmatter(content: string): ParsedFrontmatter {
|
|
7
|
+
if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
|
|
8
|
+
return { frontmatter: {}, body: content };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const normalized = content.replaceAll("\r\n", "\n");
|
|
12
|
+
const end = normalized.indexOf("\n---\n", 4);
|
|
13
|
+
if (end === -1) return { frontmatter: {}, body: content };
|
|
14
|
+
|
|
15
|
+
const raw = normalized.slice(4, end);
|
|
16
|
+
const body = normalized.slice(end + "\n---\n".length);
|
|
17
|
+
const frontmatter: Record<string, string> = {};
|
|
18
|
+
|
|
19
|
+
for (const line of raw.split("\n")) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
22
|
+
const separator = trimmed.indexOf(":");
|
|
23
|
+
if (separator === -1) continue;
|
|
24
|
+
const key = trimmed.slice(0, separator).trim();
|
|
25
|
+
const value = trimmed.slice(separator + 1).trim();
|
|
26
|
+
if (key) frontmatter[key] = value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { frontmatter, body };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseCsv(value: string | undefined): string[] | undefined {
|
|
33
|
+
if (value === undefined) return undefined;
|
|
34
|
+
const values = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
35
|
+
return values.length > 0 ? [...new Set(values)] : undefined;
|
|
36
|
+
}
|