@fitchmultz/pi-subagents 0.0.0-stage → 0.44.5
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 +1654 -0
- package/LICENSE +21 -0
- package/README.md +1533 -2
- package/agents/context-builder.md +97 -0
- package/agents/debugger.md +51 -0
- package/agents/delegate.md +13 -0
- package/agents/fixer.md +37 -0
- package/agents/oracle.md +71 -0
- package/agents/planner.md +72 -0
- package/agents/researcher.md +51 -0
- package/agents/reviewer-claude.md +69 -0
- package/agents/reviewer-gpt.md +68 -0
- package/agents/reviewer-ponytail.md +82 -0
- package/agents/reviewer-security.md +81 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +66 -0
- package/agents/ui-designer.md +51 -0
- package/agents/watcher.md +26 -0
- package/agents/worker.md +46 -0
- package/agents/writer.md +36 -0
- package/dist/agents/agent-management.js +923 -0
- package/dist/agents/agent-scope.js +7 -0
- package/dist/agents/agent-selection.js +20 -0
- package/dist/agents/agent-serializer.js +100 -0
- package/dist/agents/agents.js +752 -0
- package/dist/agents/chain-serializer.js +367 -0
- package/dist/agents/frontmatter.js +21 -0
- package/dist/agents/identity.js +29 -0
- package/dist/agents/skills.js +570 -0
- package/dist/extension/build-info.js +1 -0
- package/dist/extension/compact-tools.js +112 -0
- package/dist/extension/config.js +22 -0
- package/dist/extension/control-notices.js +29 -0
- package/dist/extension/doctor.js +143 -0
- package/dist/extension/fanout-child.js +326 -0
- package/dist/extension/index.js +577 -0
- package/dist/extension/schemas.js +416 -0
- package/dist/extension/tool-input.js +20 -0
- package/dist/extension/tool-result.js +14 -0
- package/dist/history/canonical-result.js +81 -0
- package/dist/history/database.js +72 -0
- package/dist/history/index.js +199 -0
- package/dist/history/ingest.js +468 -0
- package/dist/history/queries.js +350 -0
- package/dist/history/store.js +216 -0
- package/dist/history/types.js +4 -0
- package/dist/history/worker.js +466 -0
- package/dist/intercom/intercom-bridge.js +112 -0
- package/dist/intercom/live-intercom.js +94 -0
- package/dist/intercom/result-intercom.js +373 -0
- package/dist/pi-intercom/agent-dir.js +10 -0
- package/dist/pi-intercom/bridge.js +750 -0
- package/dist/pi-intercom/broker/broker.js +426 -0
- package/dist/pi-intercom/broker/client.js +492 -0
- package/dist/pi-intercom/broker/framing.js +65 -0
- package/dist/pi-intercom/broker/paths.js +42 -0
- package/dist/pi-intercom/broker/pid.js +66 -0
- package/dist/pi-intercom/broker/spawn.js +221 -0
- package/dist/pi-intercom/config.js +102 -0
- package/dist/pi-intercom/index.js +2942 -0
- package/dist/pi-intercom/reply-tracker.js +182 -0
- package/dist/pi-intercom/session-targets.js +128 -0
- package/dist/pi-intercom/subagent-live-events.js +132 -0
- package/dist/pi-intercom/topics.js +157 -0
- package/dist/pi-intercom/types.js +163 -0
- package/dist/pi-intercom/ui/compose.js +269 -0
- package/dist/pi-intercom/ui/inline-message.js +80 -0
- package/dist/pi-intercom/ui/session-list.js +124 -0
- package/dist/runs/background/async-control.js +95 -0
- package/dist/runs/background/async-execution.js +676 -0
- package/dist/runs/background/async-job-tracker.js +383 -0
- package/dist/runs/background/async-result-file.js +58 -0
- package/dist/runs/background/async-resume.js +305 -0
- package/dist/runs/background/async-status.js +442 -0
- package/dist/runs/background/completion-dedupe.js +69 -0
- package/dist/runs/background/completion-delivery.js +245 -0
- package/dist/runs/background/notify.js +71 -0
- package/dist/runs/background/parallel-groups.js +55 -0
- package/dist/runs/background/result-watcher.js +426 -0
- package/dist/runs/background/run-id-resolver.js +64 -0
- package/dist/runs/background/run-status.js +382 -0
- package/dist/runs/background/stale-run-reconciler.js +335 -0
- package/dist/runs/background/subagent-runner-launcher.js +55 -0
- package/dist/runs/background/subagent-runner.js +1872 -0
- package/dist/runs/background/top-level-async.js +5 -0
- package/dist/runs/foreground/chain-clarify.js +1152 -0
- package/dist/runs/foreground/clarify-invocation.js +72 -0
- package/dist/runs/foreground/execution-input.js +423 -0
- package/dist/runs/foreground/foreground-control.js +817 -0
- package/dist/runs/foreground/question-control.js +88 -0
- package/dist/runs/foreground/run-async-path.js +205 -0
- package/dist/runs/foreground/subagent-executor.js +442 -0
- package/dist/runs/foreground/subagent-params.js +165 -0
- package/dist/runs/foreground/wait-run.js +141 -0
- package/dist/runs/shared/acceptance-contract.js +253 -0
- package/dist/runs/shared/acceptance-evaluation.js +256 -0
- package/dist/runs/shared/acceptance-finalization.js +280 -0
- package/dist/runs/shared/acceptance-reports.js +147 -0
- package/dist/runs/shared/acceptance.js +4 -0
- package/dist/runs/shared/chain-outputs.js +110 -0
- package/dist/runs/shared/child-attempt.js +693 -0
- package/dist/runs/shared/child-execution-cwd.js +96 -0
- package/dist/runs/shared/child-observations.js +114 -0
- package/dist/runs/shared/claude-code.js +199 -0
- package/dist/runs/shared/completion-guard.js +31 -0
- package/dist/runs/shared/dynamic-fanout.js +274 -0
- package/dist/runs/shared/history-index.js +72 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
- package/dist/runs/shared/model-fallback.js +185 -0
- package/dist/runs/shared/mutating-tool-guard.js +170 -0
- package/dist/runs/shared/native-finalization.js +101 -0
- package/dist/runs/shared/native-usage.js +153 -0
- package/dist/runs/shared/nested-events.js +700 -0
- package/dist/runs/shared/nested-path.js +48 -0
- package/dist/runs/shared/nested-render.js +108 -0
- package/dist/runs/shared/parallel-utils.js +61 -0
- package/dist/runs/shared/parent-receipts.js +68 -0
- package/dist/runs/shared/parent-usage.js +121 -0
- package/dist/runs/shared/pi-args.js +310 -0
- package/dist/runs/shared/pi-spawn.js +52 -0
- package/dist/runs/shared/run-history-queries.js +45 -0
- package/dist/runs/shared/run-history.js +110 -0
- package/dist/runs/shared/run-id-ambiguity.js +4 -0
- package/dist/runs/shared/run-records.js +679 -0
- package/dist/runs/shared/session-cwd-preload.js +33 -0
- package/dist/runs/shared/single-output.js +222 -0
- package/dist/runs/shared/streaming-text.js +15 -0
- package/dist/runs/shared/structured-output.js +55 -0
- package/dist/runs/shared/subagent-control.js +179 -0
- package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
- package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
- package/dist/runs/shared/supervisor-questions.js +350 -0
- package/dist/runs/shared/workflow-graph.js +214 -0
- package/dist/runs/shared/workflow-policy.js +84 -0
- package/dist/runs/shared/worktree.js +525 -0
- package/dist/shared/agent-context-policy.js +126 -0
- package/dist/shared/artifacts.js +96 -0
- package/dist/shared/atomic-json.js +13 -0
- package/dist/shared/cooperative.js +20 -0
- package/dist/shared/execution-cwd.js +14 -0
- package/dist/shared/file-coalescer.js +21 -0
- package/dist/shared/fork-context.js +49 -0
- package/dist/shared/formatters.js +85 -0
- package/dist/shared/journal-reader.js +407 -0
- package/dist/shared/lazy-tools.js +32 -0
- package/dist/shared/model-info.js +68 -0
- package/dist/shared/native-session.js +15 -0
- package/dist/shared/native-tui.js +18 -0
- package/dist/shared/native-typebox.js +20 -0
- package/dist/shared/post-exit-stdio-guard.js +197 -0
- package/dist/shared/prompt-sections.js +8 -0
- package/dist/shared/session-entries.js +52 -0
- package/dist/shared/session-identity.js +14 -0
- package/dist/shared/settings.js +254 -0
- package/dist/shared/status-format.js +111 -0
- package/dist/shared/temp-root.js +114 -0
- package/dist/shared/types.js +207 -0
- package/dist/shared/ui-mode.js +3 -0
- package/dist/shared/utils.js +334 -0
- package/dist/slash/prompt-template-bridge.js +298 -0
- package/dist/slash/slash-bridge.js +116 -0
- package/dist/slash/slash-commands.js +607 -0
- package/dist/slash/slash-live-state.js +246 -0
- package/dist/tui/action-hints.js +75 -0
- package/dist/tui/agent-history.js +197 -0
- package/dist/tui/agent-view.js +1656 -0
- package/dist/tui/render.js +1515 -0
- package/docs/intercom-bridge.md +212 -0
- package/docs/intercom.md +581 -0
- package/docs/performance.md +121 -0
- package/package.json +102 -4
- package/prompts/gather-context-and-clarify.md +14 -0
- package/prompts/parallel-cleanup.md +62 -0
- package/prompts/parallel-context-build.md +56 -0
- package/prompts/parallel-handoff-plan.md +62 -0
- package/prompts/parallel-research.md +51 -0
- package/prompts/parallel-review.md +59 -0
- package/prompts/review-loop.md +42 -0
- package/scripts/build.mjs +134 -0
- package/scripts/check-compat.mjs +48 -0
- package/scripts/compat-host.mjs +19 -0
- package/scripts/intercom-bridge-credentials.mjs +143 -0
- package/scripts/intercom-remote.mjs +103 -0
- package/scripts/linux-smoke.sh +51 -0
- package/scripts/local-install-smoke.mjs +66 -0
- package/scripts/native-package-smoke.mjs +82 -0
- package/scripts/package-smoke.mjs +209 -0
- package/scripts/prepare.mjs +67 -0
- package/scripts/real-pi-smoke.mjs +415 -0
- package/scripts/run-tests.mjs +149 -0
- package/skills/pi-intercom/SKILL.md +113 -0
- package/skills/pi-intercom/evals/evals.json +29 -0
- package/skills/pi-intercom/evals/trigger-evals.json +47 -0
- package/skills/pi-intercom/references/peer-sessions.md +69 -0
- package/skills/pi-subagents/SKILL.md +76 -0
- package/skills/pi-subagents/evals/evals.json +29 -0
- package/skills/pi-subagents/evals/trigger-evals.json +42 -0
- package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
- package/src/agents/agent-management.ts +813 -0
- package/src/agents/agent-scope.ts +7 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +99 -0
- package/src/agents/agents.ts +916 -0
- package/src/agents/chain-serializer.ts +329 -0
- package/src/agents/frontmatter.ts +21 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +637 -0
- package/src/extension/build-info.ts +2 -0
- package/src/extension/compact-tools.ts +114 -0
- package/src/extension/config.ts +23 -0
- package/src/extension/control-notices.ts +47 -0
- package/src/extension/doctor.ts +171 -0
- package/src/extension/fanout-child.ts +312 -0
- package/src/extension/index.ts +649 -0
- package/src/extension/schemas.ts +444 -0
- package/src/extension/tool-input.ts +19 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/history/canonical-result.ts +62 -0
- package/src/history/database.ts +59 -0
- package/src/history/index.ts +156 -0
- package/src/history/ingest.ts +349 -0
- package/src/history/queries.ts +241 -0
- package/src/history/store.ts +203 -0
- package/src/history/types.ts +166 -0
- package/src/history/worker.ts +321 -0
- package/src/intercom/intercom-bridge.ts +123 -0
- package/src/intercom/live-intercom.ts +104 -0
- package/src/intercom/result-intercom.ts +429 -0
- package/src/pi-intercom/agent-dir.ts +9 -0
- package/src/pi-intercom/bridge.ts +668 -0
- package/src/pi-intercom/broker/broker.ts +463 -0
- package/src/pi-intercom/broker/client.ts +558 -0
- package/src/pi-intercom/broker/framing.ts +77 -0
- package/src/pi-intercom/broker/paths.ts +47 -0
- package/src/pi-intercom/broker/pid.ts +59 -0
- package/src/pi-intercom/broker/spawn.ts +248 -0
- package/src/pi-intercom/config.ts +142 -0
- package/src/pi-intercom/index.ts +2929 -0
- package/src/pi-intercom/reply-tracker.ts +211 -0
- package/src/pi-intercom/session-targets.ts +172 -0
- package/src/pi-intercom/subagent-live-events.ts +133 -0
- package/src/pi-intercom/topics.ts +125 -0
- package/src/pi-intercom/types.ts +271 -0
- package/src/pi-intercom/ui/compose.ts +289 -0
- package/src/pi-intercom/ui/inline-message.ts +92 -0
- package/src/pi-intercom/ui/session-list.ts +139 -0
- package/src/runs/background/async-control.ts +89 -0
- package/src/runs/background/async-execution.ts +850 -0
- package/src/runs/background/async-job-tracker.ts +385 -0
- package/src/runs/background/async-result-file.ts +62 -0
- package/src/runs/background/async-resume.ts +333 -0
- package/src/runs/background/async-status.ts +481 -0
- package/src/runs/background/completion-dedupe.ts +79 -0
- package/src/runs/background/completion-delivery.ts +195 -0
- package/src/runs/background/notify.ts +123 -0
- package/src/runs/background/parallel-groups.ts +53 -0
- package/src/runs/background/result-watcher.ts +423 -0
- package/src/runs/background/run-id-resolver.ts +69 -0
- package/src/runs/background/run-status.ts +384 -0
- package/src/runs/background/stale-run-reconciler.ts +369 -0
- package/src/runs/background/subagent-runner-launcher.ts +56 -0
- package/src/runs/background/subagent-runner.ts +2053 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1340 -0
- package/src/runs/foreground/clarify-invocation.ts +77 -0
- package/src/runs/foreground/execution-input.ts +467 -0
- package/src/runs/foreground/foreground-control.ts +821 -0
- package/src/runs/foreground/question-control.ts +82 -0
- package/src/runs/foreground/run-async-path.ts +243 -0
- package/src/runs/foreground/subagent-executor.ts +501 -0
- package/src/runs/foreground/subagent-params.ts +320 -0
- package/src/runs/foreground/wait-run.ts +124 -0
- package/src/runs/shared/acceptance-contract.ts +257 -0
- package/src/runs/shared/acceptance-evaluation.ts +275 -0
- package/src/runs/shared/acceptance-finalization.ts +353 -0
- package/src/runs/shared/acceptance-reports.ts +152 -0
- package/src/runs/shared/acceptance.ts +33 -0
- package/src/runs/shared/chain-outputs.ts +114 -0
- package/src/runs/shared/child-attempt.ts +623 -0
- package/src/runs/shared/child-execution-cwd.ts +91 -0
- package/src/runs/shared/child-observations.ts +99 -0
- package/src/runs/shared/claude-code.ts +243 -0
- package/src/runs/shared/completion-guard.ts +35 -0
- package/src/runs/shared/dynamic-fanout.ts +295 -0
- package/src/runs/shared/history-index.ts +69 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
- package/src/runs/shared/model-fallback.ts +219 -0
- package/src/runs/shared/mutating-tool-guard.ts +194 -0
- package/src/runs/shared/native-finalization.ts +128 -0
- package/src/runs/shared/native-usage.ts +113 -0
- package/src/runs/shared/nested-events.ts +752 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +119 -0
- package/src/runs/shared/parallel-utils.ts +153 -0
- package/src/runs/shared/parent-receipts.ts +59 -0
- package/src/runs/shared/parent-usage.ts +121 -0
- package/src/runs/shared/pi-args.ts +350 -0
- package/src/runs/shared/pi-spawn.ts +52 -0
- package/src/runs/shared/run-history-queries.ts +44 -0
- package/src/runs/shared/run-history.ts +100 -0
- package/src/runs/shared/run-id-ambiguity.ts +4 -0
- package/src/runs/shared/run-records.ts +607 -0
- package/src/runs/shared/session-cwd-preload.ts +27 -0
- package/src/runs/shared/single-output.ts +267 -0
- package/src/runs/shared/streaming-text.ts +18 -0
- package/src/runs/shared/structured-output.ts +68 -0
- package/src/runs/shared/subagent-control.ts +219 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
- package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
- package/src/runs/shared/supervisor-questions.ts +415 -0
- package/src/runs/shared/workflow-graph.ts +220 -0
- package/src/runs/shared/workflow-policy.ts +125 -0
- package/src/runs/shared/worktree.ts +635 -0
- package/src/shared/agent-context-policy.ts +184 -0
- package/src/shared/artifacts.ts +95 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/cooperative.ts +21 -0
- package/src/shared/execution-cwd.ts +14 -0
- package/src/shared/file-coalescer.ts +29 -0
- package/src/shared/fork-context.ts +68 -0
- package/src/shared/formatters.ts +84 -0
- package/src/shared/journal-reader.ts +333 -0
- package/src/shared/lazy-tools.ts +36 -0
- package/src/shared/model-info.ts +81 -0
- package/src/shared/native-session.ts +8 -0
- package/src/shared/native-tui.ts +10 -0
- package/src/shared/native-typebox.ts +14 -0
- package/src/shared/post-exit-stdio-guard.ts +204 -0
- package/src/shared/prompt-sections.ts +10 -0
- package/src/shared/session-entries.ts +49 -0
- package/src/shared/session-identity.ts +18 -0
- package/src/shared/settings.ts +384 -0
- package/src/shared/status-format.ts +123 -0
- package/src/shared/temp-root.ts +107 -0
- package/src/shared/types.ts +1260 -0
- package/src/shared/ui-mode.ts +5 -0
- package/src/shared/utils.ts +342 -0
- package/src/slash/prompt-template-bridge.ts +408 -0
- package/src/slash/slash-bridge.ts +178 -0
- package/src/slash/slash-commands.ts +617 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/action-hints.ts +69 -0
- package/src/tui/agent-history.ts +210 -0
- package/src/tui/agent-view.ts +1323 -0
- package/src/tui/render.ts +1550 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { AgentConfig } from "../agents/agents.ts";
|
|
2
|
+
import type { ChainStep, SequentialStep } from "./settings.ts";
|
|
3
|
+
import { getStepAgents, isDynamicParallelStep, isParallelStep } from "./settings.ts";
|
|
4
|
+
import { createForkContextResolver, resolveSubagentContext } from "./fork-context.ts";
|
|
5
|
+
import { wrapForkTask } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export type SubagentExecutionContext = "fresh" | "fork";
|
|
8
|
+
|
|
9
|
+
interface ForkableSessionManager {
|
|
10
|
+
getSessionFile(): string | undefined;
|
|
11
|
+
getLeafId(): string | null;
|
|
12
|
+
getSessionDir(): string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SubagentParamsLikeForContext {
|
|
16
|
+
agent?: string;
|
|
17
|
+
model?: string;
|
|
18
|
+
tasks?: Array<{ agent: string; model?: string }>;
|
|
19
|
+
chain?: ChainStep[];
|
|
20
|
+
context?: SubagentExecutionContext;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface InvocationAgentTarget {
|
|
24
|
+
agent: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveAgentContext(
|
|
29
|
+
explicitContext: unknown,
|
|
30
|
+
agentName: string | undefined,
|
|
31
|
+
agents: readonly AgentConfig[],
|
|
32
|
+
): SubagentExecutionContext {
|
|
33
|
+
if (explicitContext !== undefined) {
|
|
34
|
+
return resolveSubagentContext(explicitContext);
|
|
35
|
+
}
|
|
36
|
+
if (!agentName) return "fresh";
|
|
37
|
+
const agent = agents.find((entry) => entry.name === agentName);
|
|
38
|
+
return agent?.defaultContext === "fork" ? "fork" : "fresh";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function collectInvocationAgentTargets(params: SubagentParamsLikeForContext): InvocationAgentTarget[] {
|
|
42
|
+
if (params.tasks?.length) {
|
|
43
|
+
return params.tasks.map((task) => ({ agent: task.agent, model: task.model }));
|
|
44
|
+
}
|
|
45
|
+
if (params.chain?.length) {
|
|
46
|
+
const targets: InvocationAgentTarget[] = [];
|
|
47
|
+
for (const step of params.chain) {
|
|
48
|
+
if (isParallelStep(step)) {
|
|
49
|
+
for (const task of step.parallel) targets.push({ agent: task.agent, model: task.model });
|
|
50
|
+
} else if (isDynamicParallelStep(step)) {
|
|
51
|
+
targets.push({ agent: step.parallel.agent, model: step.parallel.model });
|
|
52
|
+
} else {
|
|
53
|
+
targets.push({ agent: step.agent, model: step.model });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return targets;
|
|
57
|
+
}
|
|
58
|
+
return params.agent ? [{ agent: params.agent, model: params.model }] : [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateForkContextModelPolicy(
|
|
62
|
+
params: SubagentParamsLikeForContext,
|
|
63
|
+
agents: readonly AgentConfig[],
|
|
64
|
+
resolveModel?: (model: string) => string | undefined,
|
|
65
|
+
): string | undefined {
|
|
66
|
+
for (const target of collectInvocationAgentTargets(params)) {
|
|
67
|
+
const agent = agents.find((entry) => entry.name === target.agent);
|
|
68
|
+
if (!agent || resolveAgentContext(params.context, target.agent, agents) !== "fork") continue;
|
|
69
|
+
const anthropicModel = (target.model ? [target.model] : [agent.model, ...(agent.fallbackModels ?? [])])
|
|
70
|
+
.filter((model): model is string => Boolean(model?.trim()))
|
|
71
|
+
.map((model) => resolveModel?.(model) ?? model)
|
|
72
|
+
.find((model) => model.trim().toLowerCase().startsWith("anthropic/"));
|
|
73
|
+
if (anthropicModel) {
|
|
74
|
+
return `Fork context cannot be used with anthropic/* models. Agent '${target.agent}' has effective model candidate '${anthropicModel}'. Use context: "fresh" or a non-Anthropic model; this restriction cannot be overridden.`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function invocationUsesForkContext(
|
|
81
|
+
explicitContext: unknown,
|
|
82
|
+
agentNames: readonly string[],
|
|
83
|
+
agents: readonly AgentConfig[],
|
|
84
|
+
): boolean {
|
|
85
|
+
if (explicitContext !== undefined) {
|
|
86
|
+
return resolveSubagentContext(explicitContext) === "fork";
|
|
87
|
+
}
|
|
88
|
+
return agentNames.some((name) => resolveAgentContext(undefined, name, agents) === "fork");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildFlatAgentNameResolver(params: SubagentParamsLikeForContext): (index: number) => string | undefined {
|
|
92
|
+
if (params.agent && !params.tasks?.length && !params.chain?.length) {
|
|
93
|
+
return () => params.agent;
|
|
94
|
+
}
|
|
95
|
+
if (params.tasks?.length) {
|
|
96
|
+
return (index) => params.tasks![index]?.agent;
|
|
97
|
+
}
|
|
98
|
+
if (params.chain?.length) {
|
|
99
|
+
const flatAgents: string[] = [];
|
|
100
|
+
for (const step of params.chain) {
|
|
101
|
+
if (isParallelStep(step)) {
|
|
102
|
+
for (const task of step.parallel) flatAgents.push(task.agent);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
flatAgents.push(...getStepAgents(step));
|
|
106
|
+
}
|
|
107
|
+
return (index) => flatAgents[index];
|
|
108
|
+
}
|
|
109
|
+
return () => undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function wrapTaskForAgentContext(
|
|
113
|
+
task: string,
|
|
114
|
+
explicitContext: unknown,
|
|
115
|
+
agentName: string | undefined,
|
|
116
|
+
agents: readonly AgentConfig[],
|
|
117
|
+
): string {
|
|
118
|
+
return resolveAgentContext(explicitContext, agentName, agents) === "fork" ? wrapForkTask(task) : task;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function wrapChainTasksForAgentContext(
|
|
122
|
+
chain: ChainStep[],
|
|
123
|
+
explicitContext: unknown,
|
|
124
|
+
agents: readonly AgentConfig[],
|
|
125
|
+
): ChainStep[] {
|
|
126
|
+
return chain.map((step, stepIndex) => {
|
|
127
|
+
if (isParallelStep(step)) {
|
|
128
|
+
return {
|
|
129
|
+
...step,
|
|
130
|
+
parallel: step.parallel.map((task) => ({
|
|
131
|
+
...task,
|
|
132
|
+
task: wrapTaskForAgentContext(task.task ?? "{previous}", explicitContext, task.agent, agents),
|
|
133
|
+
})),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (isDynamicParallelStep(step)) {
|
|
137
|
+
return {
|
|
138
|
+
...step,
|
|
139
|
+
parallel: {
|
|
140
|
+
...step.parallel,
|
|
141
|
+
task: wrapTaskForAgentContext(
|
|
142
|
+
step.parallel.task ?? "{previous}",
|
|
143
|
+
explicitContext,
|
|
144
|
+
step.parallel.agent,
|
|
145
|
+
agents,
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const sequential = step as SequentialStep;
|
|
151
|
+
const agentName = getStepAgents(step)[0];
|
|
152
|
+
return {
|
|
153
|
+
...sequential,
|
|
154
|
+
task: wrapTaskForAgentContext(
|
|
155
|
+
sequential.task ?? (stepIndex === 0 ? "{task}" : "{previous}"),
|
|
156
|
+
explicitContext,
|
|
157
|
+
agentName,
|
|
158
|
+
agents,
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function createPerAgentForkContextResolver(
|
|
165
|
+
sessionManager: ForkableSessionManager,
|
|
166
|
+
resolveContextForIndex: (index?: number) => SubagentExecutionContext,
|
|
167
|
+
options: { resolveContextForAgentIndex?: (agentName: string | undefined, index?: number) => SubagentExecutionContext } = {},
|
|
168
|
+
): { sessionFileForIndex(index?: number): string | undefined; sessionFileForAgentIndex(agentName: string | undefined, index?: number): string | undefined } {
|
|
169
|
+
let forkResolver: ReturnType<typeof createForkContextResolver> | undefined;
|
|
170
|
+
const sessionFileForContext = (context: SubagentExecutionContext, index = 0): string | undefined => {
|
|
171
|
+
if (context !== "fork") return undefined;
|
|
172
|
+
if (!forkResolver) forkResolver = createForkContextResolver(sessionManager, "fork");
|
|
173
|
+
return forkResolver.sessionFileForIndex(index);
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
sessionFileForIndex(index = 0): string | undefined {
|
|
177
|
+
return sessionFileForContext(resolveContextForIndex(index), index);
|
|
178
|
+
},
|
|
179
|
+
sessionFileForAgentIndex(agentName, index = 0): string | undefined {
|
|
180
|
+
const context = options.resolveContextForAgentIndex?.(agentName, index) ?? resolveContextForIndex(index);
|
|
181
|
+
return sessionFileForContext(context, index);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { TEMP_ARTIFACTS_DIR, type ArtifactPaths } from "./types.ts";
|
|
4
|
+
import { getAgentDir } from "./utils.ts";
|
|
5
|
+
import { ensureSafeTempPath } from "./temp-root.ts";
|
|
6
|
+
const CLEANUP_MARKER_FILE = ".last-cleanup";
|
|
7
|
+
export const ARTIFACT_CLEANUP_DAYS = 7;
|
|
8
|
+
|
|
9
|
+
export function getArtifactsDir(sessionFile: string | null): string {
|
|
10
|
+
if (sessionFile) {
|
|
11
|
+
const sessionDir = path.dirname(sessionFile);
|
|
12
|
+
return path.join(sessionDir, "subagent-artifacts");
|
|
13
|
+
}
|
|
14
|
+
return TEMP_ARTIFACTS_DIR;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getArtifactPaths(artifactsDir: string, runId: string, agent: string, index?: number): ArtifactPaths {
|
|
18
|
+
ensureSafeTempPath(artifactsDir);
|
|
19
|
+
const suffix = index !== undefined ? `_${index}` : "";
|
|
20
|
+
const safeAgent = agent.replace(/[^\w.-]/g, "_").slice(0, 128);
|
|
21
|
+
const base = `${runId}_${safeAgent}${suffix}`;
|
|
22
|
+
return {
|
|
23
|
+
inputPath: path.join(artifactsDir, `${base}_input.md`),
|
|
24
|
+
outputPath: path.join(artifactsDir, `${base}_output.md`),
|
|
25
|
+
metadataPath: path.join(artifactsDir, `${base}_meta.json`),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function appendJsonl(filePath: string, line: string): void {
|
|
30
|
+
fs.appendFileSync(filePath, `${line}\n`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function cleanupOldArtifacts(dir: string, maxAgeDays: number): Promise<void> {
|
|
34
|
+
ensureSafeTempPath(dir);
|
|
35
|
+
try { await fs.promises.access(dir); } catch { return; }
|
|
36
|
+
|
|
37
|
+
const markerPath = path.join(dir, CLEANUP_MARKER_FILE);
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const stat = await fs.promises.lstat(markerPath);
|
|
42
|
+
if (!stat.isSymbolicLink() && now - stat.mtimeMs < 24 * 60 * 60 * 1000) return;
|
|
43
|
+
await fs.promises.unlink(markerPath);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
49
|
+
const cutoff = now - maxAgeMs;
|
|
50
|
+
|
|
51
|
+
for (const file of await fs.promises.readdir(dir)) {
|
|
52
|
+
if (file === CLEANUP_MARKER_FILE) continue;
|
|
53
|
+
const filePath = path.join(dir, file);
|
|
54
|
+
try {
|
|
55
|
+
const stat = await fs.promises.lstat(filePath);
|
|
56
|
+
if (stat.mtimeMs < cutoff) {
|
|
57
|
+
await fs.promises.unlink(filePath);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Artifact cleanup is best-effort housekeeping. Skip files that disappear
|
|
61
|
+
// or become unreadable while scanning so one bad entry does not block the rest.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await fs.promises.writeFile(markerPath, String(now), { flag: "wx", mode: 0o600 });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function cleanupAllArtifactDirs(maxAgeDays: number): Promise<void> {
|
|
73
|
+
await cleanupOldArtifacts(TEMP_ARTIFACTS_DIR, maxAgeDays);
|
|
74
|
+
|
|
75
|
+
const sessionsBase = path.join(getAgentDir(), "sessions");
|
|
76
|
+
|
|
77
|
+
let dirs: string[];
|
|
78
|
+
try {
|
|
79
|
+
dirs = await fs.promises.readdir(sessionsBase);
|
|
80
|
+
} catch {
|
|
81
|
+
// Session artifact cleanup is best-effort. If the sessions root cannot be read,
|
|
82
|
+
// skip cleanup instead of failing extension startup.
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const dir of dirs) {
|
|
87
|
+
const artifactsDir = path.join(sessionsBase, dir, "subagent-artifacts");
|
|
88
|
+
try {
|
|
89
|
+
await cleanupOldArtifacts(artifactsDir, maxAgeDays);
|
|
90
|
+
} catch {
|
|
91
|
+
// Session cleanup is best-effort. Keep going so one unreadable session dir
|
|
92
|
+
// does not block cleanup for the rest.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export function writeAtomicJson(filePath: string, payload: object): void {
|
|
5
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
6
|
+
const tempPath = path.join(
|
|
7
|
+
path.dirname(filePath),
|
|
8
|
+
`.${path.basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`,
|
|
9
|
+
);
|
|
10
|
+
try {
|
|
11
|
+
fs.writeFileSync(tempPath, JSON.stringify(payload, null, 2), { encoding: "utf-8", mode: 0o600 });
|
|
12
|
+
fs.renameSync(tempPath, filePath);
|
|
13
|
+
} finally {
|
|
14
|
+
fs.rmSync(tempPath, { force: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { setImmediate as yieldToInput } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
export function runSynchronously<T>(steps: Generator<void, T>): T {
|
|
4
|
+
let step = steps.next();
|
|
5
|
+
while (!step.done) step = steps.next();
|
|
6
|
+
return step.value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Batched recovery and delivery leave room for input between records. */
|
|
10
|
+
export async function runCooperatively<T>(steps: Generator<void, T>): Promise<T> {
|
|
11
|
+
let step = steps.next();
|
|
12
|
+
for (let count = 0; !step.done; step = steps.next()) {
|
|
13
|
+
// ponytail: one recovery record remains atomic; move a demonstrated
|
|
14
|
+
// single-record stall off-thread rather than increasing this batch size.
|
|
15
|
+
if (++count === 32) {
|
|
16
|
+
await yieldToInput();
|
|
17
|
+
count = 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return step.value;
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { hasExecutionCwdOwner } from "../runs/shared/child-execution-cwd.ts";
|
|
3
|
+
|
|
4
|
+
/** Capture the active directory owner once per invocation, before asynchronous preparation. */
|
|
5
|
+
export function resolveExecutionCwd(pi: Pick<ExtensionAPI, "events"> & Partial<Pick<ExtensionAPI, "getAllTools" | "getCommands">>, ctx: ExtensionContext): string {
|
|
6
|
+
const request: { sessionManager: ExtensionContext["sessionManager"]; result?: { cwd?: string; error?: string } } = { sessionManager: ctx.sessionManager };
|
|
7
|
+
pi.events.emit("pi-change-working-dir:resolve-execution-cwd", request);
|
|
8
|
+
if (request.result?.error) throw new Error(request.result.error);
|
|
9
|
+
if (request.result && !request.result.cwd) throw new Error("Working-directory owner did not return an execution directory.");
|
|
10
|
+
if (!request.result && pi.getAllTools && pi.getCommands && hasExecutionCwdOwner({ getAllTools: pi.getAllTools, getCommands: pi.getCommands })) {
|
|
11
|
+
throw new Error("The loaded directory extension did not resolve the execution directory. Update pi-change-working-dir before delegating work.");
|
|
12
|
+
}
|
|
13
|
+
return request.result?.cwd ?? ctx.cwd;
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
interface FileCoalescer {
|
|
2
|
+
schedule(file: string, delayMs?: number): boolean;
|
|
3
|
+
clear(): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createFileCoalescer(
|
|
7
|
+
handler: (file: string) => void,
|
|
8
|
+
defaultDelayMs: number,
|
|
9
|
+
): FileCoalescer {
|
|
10
|
+
const pending = new Map<string, ReturnType<typeof setTimeout>>();
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
schedule(file: string, delayMs = defaultDelayMs): boolean {
|
|
14
|
+
if (pending.has(file)) return false;
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
pending.delete(file);
|
|
17
|
+
handler(file);
|
|
18
|
+
}, delayMs);
|
|
19
|
+
pending.set(file, timer);
|
|
20
|
+
return true;
|
|
21
|
+
},
|
|
22
|
+
clear(): void {
|
|
23
|
+
for (const timer of pending.values()) {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
}
|
|
26
|
+
pending.clear();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { requestChildExecutionCwd } from "../runs/shared/child-execution-cwd.ts";
|
|
4
|
+
|
|
5
|
+
type SubagentExecutionContext = "fresh" | "fork";
|
|
6
|
+
|
|
7
|
+
interface ForkableSessionManager {
|
|
8
|
+
getSessionFile(): string | undefined;
|
|
9
|
+
getLeafId(): string | null;
|
|
10
|
+
getSessionDir(): string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ForkContextResolver {
|
|
14
|
+
sessionFileForIndex(index?: number): string | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveSubagentContext(value: unknown): SubagentExecutionContext {
|
|
18
|
+
return value === "fork" ? "fork" : "fresh";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createForkContextResolver(
|
|
22
|
+
sessionManager: ForkableSessionManager,
|
|
23
|
+
requestedContext: unknown,
|
|
24
|
+
): ForkContextResolver {
|
|
25
|
+
if (resolveSubagentContext(requestedContext) !== "fork") {
|
|
26
|
+
return {
|
|
27
|
+
sessionFileForIndex: () => undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parentSessionFile = sessionManager.getSessionFile();
|
|
32
|
+
if (!parentSessionFile) {
|
|
33
|
+
throw new Error("Forked subagent context requires a persisted parent session.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const leafId = sessionManager.getLeafId();
|
|
37
|
+
if (!leafId) {
|
|
38
|
+
throw new Error("Forked subagent context requires a current leaf to fork from.");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sessionDir = sessionManager.getSessionDir();
|
|
42
|
+
const cachedSessionFiles = new Map<number, string>();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
sessionFileForIndex(index = 0): string | undefined {
|
|
46
|
+
const cached = cachedSessionFiles.get(index);
|
|
47
|
+
if (cached) return cached;
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(parentSessionFile)) {
|
|
50
|
+
throw new Error(`Parent session file does not exist: ${parentSessionFile}. Pi has not persisted enough history to fork yet.`);
|
|
51
|
+
}
|
|
52
|
+
const sessionFile = SessionManager.open(parentSessionFile, sessionDir).createBranchedSession(leafId);
|
|
53
|
+
if (!sessionFile) {
|
|
54
|
+
throw new Error("Session manager did not return a forked session file.");
|
|
55
|
+
}
|
|
56
|
+
if (!fs.existsSync(sessionFile)) {
|
|
57
|
+
throw new Error(`Session manager returned a forked session file that does not exist: ${sessionFile}`);
|
|
58
|
+
}
|
|
59
|
+
requestChildExecutionCwd(sessionFile);
|
|
60
|
+
cachedSessionFiles.set(index, sessionFile);
|
|
61
|
+
return sessionFile;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
64
|
+
throw new Error(`Failed to create forked subagent session: ${cause.message}`, { cause });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatting utilities for display output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Usage } from "./types.ts";
|
|
6
|
+
import { splitKnownThinkingSuffix, THINKING_LEVELS } from "./model-info.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Format token count with k suffix for large numbers
|
|
10
|
+
*/
|
|
11
|
+
export function formatTokens(n: number): string {
|
|
12
|
+
return n < 1000 ? String(n) : n < 10000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatModelThinking(model?: string, thinking?: string): string {
|
|
16
|
+
const parsed = model ? splitKnownThinkingSuffix(model) : undefined;
|
|
17
|
+
const displayModel = parsed?.baseModel ?? model;
|
|
18
|
+
const explicitThinking = THINKING_LEVELS.find((level) => level === thinking?.trim());
|
|
19
|
+
const displayThinking = parsed?.thinkingSuffix ? parsed.thinkingSuffix.slice(1) : explicitThinking;
|
|
20
|
+
return [displayModel, displayThinking ? `thinking ${displayThinking}` : undefined].filter(Boolean).join(" · ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format usage statistics into a compact string
|
|
25
|
+
*/
|
|
26
|
+
export function formatUsage(u: Usage, model?: string): string {
|
|
27
|
+
const parts: string[] = [];
|
|
28
|
+
if (u.turns) parts.push(`${u.turns} turn${u.turns > 1 ? "s" : ""}`);
|
|
29
|
+
if (u.input) parts.push(`in:${formatTokens(u.input)}`);
|
|
30
|
+
if (u.output) parts.push(`out:${formatTokens(u.output)}`);
|
|
31
|
+
if (u.cacheRead) parts.push(`R${formatTokens(u.cacheRead)}`);
|
|
32
|
+
if (u.cacheWrite) parts.push(`W${formatTokens(u.cacheWrite)}`);
|
|
33
|
+
if (u.cost) parts.push(`$${u.cost.toFixed(4)}`);
|
|
34
|
+
if (model) parts.push(model);
|
|
35
|
+
return parts.join(" ");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format duration in human-readable form
|
|
40
|
+
*/
|
|
41
|
+
export function formatDuration(ms: number): string {
|
|
42
|
+
if (ms < 1000) return `${ms}ms`;
|
|
43
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
44
|
+
return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format a tool call for display
|
|
49
|
+
*/
|
|
50
|
+
export function formatToolCall(name: string, args: Record<string, unknown>, expanded = false): string {
|
|
51
|
+
switch (name) {
|
|
52
|
+
case "bash": {
|
|
53
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
54
|
+
const maxLength = expanded ? 240 : 60;
|
|
55
|
+
return `$ ${command.slice(0, maxLength)}${command.length > maxLength ? "..." : ""}`;
|
|
56
|
+
}
|
|
57
|
+
case "read":
|
|
58
|
+
case "write":
|
|
59
|
+
case "edit": {
|
|
60
|
+
const target = typeof args.path === "string"
|
|
61
|
+
? args.path
|
|
62
|
+
: typeof args.file_path === "string"
|
|
63
|
+
? args.file_path
|
|
64
|
+
: "";
|
|
65
|
+
return `${name} ${shortenPath(target)}`;
|
|
66
|
+
}
|
|
67
|
+
default: {
|
|
68
|
+
const s = JSON.stringify(args);
|
|
69
|
+
const maxLength = expanded ? 160 : 40;
|
|
70
|
+
return `${name} ${s.slice(0, maxLength)}${s.length > maxLength ? "..." : ""}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Shorten a path by replacing home directory with ~
|
|
77
|
+
*/
|
|
78
|
+
export function shortenPath(p: string): string {
|
|
79
|
+
const home = process.env.HOME;
|
|
80
|
+
if (home && p.startsWith(home)) {
|
|
81
|
+
return `~${p.slice(home.length)}`;
|
|
82
|
+
}
|
|
83
|
+
return p;
|
|
84
|
+
}
|