pi-subagents 0.11.2 → 0.11.3
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 +7 -0
- package/agents.ts +1 -30
- package/async-execution.ts +1 -1
- package/async-job-tracker.ts +123 -0
- package/chain-serializer.ts +1 -30
- package/execution.ts +17 -86
- package/frontmatter.ts +29 -0
- package/index.ts +69 -1169
- package/package.json +1 -1
- package/parallel-utils.ts +21 -5
- package/pi-args.ts +122 -0
- package/result-watcher.ts +92 -0
- package/settings.ts +2 -38
- package/slash-commands.ts +348 -0
- package/subagent-executor.ts +736 -0
- package/subagent-runner.ts +16 -64
- package/types.ts +18 -0
- package/utils.ts +1 -28
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.11.3] - 2026-03-17
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Decomposed `index.ts` (1,450 → ~350 lines) into focused modules: `subagent-executor.ts`, `async-job-tracker.ts`, `result-watcher.ts`, `slash-commands.ts`. Shared mutable state centralized in `SubagentState` interface. Three identical session handlers collapsed into one.
|
|
9
|
+
- Extracted shared pi CLI arg-builder (`pi-args.ts`) from duplicated logic in `execution.ts` and `subagent-runner.ts`.
|
|
10
|
+
- Consolidated `mapConcurrent` (canonical in `parallel-utils.ts`, re-exported from `utils.ts`), `aggregateParallelOutputs` (canonical in `parallel-utils.ts` with optional header formatter, re-exported from `settings.ts`), and `parseFrontmatter` (extracted to `frontmatter.ts`).
|
|
11
|
+
|
|
5
12
|
## [0.11.2] - 2026-03-11
|
|
6
13
|
|
|
7
14
|
### Fixed
|
package/agents.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { KNOWN_FIELDS } from "./agent-serializer.js";
|
|
10
10
|
import { parseChain } from "./chain-serializer.js";
|
|
11
11
|
import { mergeAgentsForScope } from "./agent-selection.js";
|
|
12
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
12
13
|
|
|
13
14
|
export type AgentScope = "user" | "project" | "both";
|
|
14
15
|
|
|
@@ -58,36 +59,6 @@ export interface AgentDiscoveryResult {
|
|
|
58
59
|
projectAgentsDir: string | null;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
62
|
-
const frontmatter: Record<string, string> = {};
|
|
63
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
64
|
-
|
|
65
|
-
if (!normalized.startsWith("---")) {
|
|
66
|
-
return { frontmatter, body: normalized };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
70
|
-
if (endIndex === -1) {
|
|
71
|
-
return { frontmatter, body: normalized };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
75
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
76
|
-
|
|
77
|
-
for (const line of frontmatterBlock.split("\n")) {
|
|
78
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
79
|
-
if (match) {
|
|
80
|
-
let value = match[2].trim();
|
|
81
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
82
|
-
value = value.slice(1, -1);
|
|
83
|
-
}
|
|
84
|
-
frontmatter[match[1]] = value;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { frontmatter, body };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
62
|
function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
92
63
|
const agents: AgentConfig[] = [];
|
|
93
64
|
|
package/async-execution.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
12
12
|
import type { AgentConfig } from "./agents.js";
|
|
13
|
-
import { applyThinkingSuffix } from "./
|
|
13
|
+
import { applyThinkingSuffix } from "./pi-args.js";
|
|
14
14
|
import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.js";
|
|
15
15
|
import { isParallelStep, resolveStepBehavior, type ChainStep, type ParallelStep, type SequentialStep, type StepOverrides } from "./settings.js";
|
|
16
16
|
import type { RunnerStep } from "./parallel-utils.js";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { renderWidget } from "./render.js";
|
|
4
|
+
import {
|
|
5
|
+
type SubagentState,
|
|
6
|
+
POLL_INTERVAL_MS,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
import { readStatus } from "./utils.js";
|
|
9
|
+
|
|
10
|
+
export function createAsyncJobTracker(state: SubagentState, asyncDirRoot: string): {
|
|
11
|
+
ensurePoller: () => void;
|
|
12
|
+
handleStarted: (data: unknown) => void;
|
|
13
|
+
handleComplete: (data: unknown) => void;
|
|
14
|
+
resetJobs: (ctx?: ExtensionContext) => void;
|
|
15
|
+
} {
|
|
16
|
+
const ensurePoller = () => {
|
|
17
|
+
if (state.poller) return;
|
|
18
|
+
state.poller = setInterval(() => {
|
|
19
|
+
if (!state.lastUiContext || !state.lastUiContext.hasUI) return;
|
|
20
|
+
if (state.asyncJobs.size === 0) {
|
|
21
|
+
renderWidget(state.lastUiContext, []);
|
|
22
|
+
if (state.poller) {
|
|
23
|
+
clearInterval(state.poller);
|
|
24
|
+
state.poller = null;
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const job of state.asyncJobs.values()) {
|
|
30
|
+
if (job.status === "complete" || job.status === "failed") {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const status = readStatus(job.asyncDir);
|
|
34
|
+
if (status) {
|
|
35
|
+
job.status = status.state;
|
|
36
|
+
job.mode = status.mode;
|
|
37
|
+
job.currentStep = status.currentStep ?? job.currentStep;
|
|
38
|
+
job.stepsTotal = status.steps?.length ?? job.stepsTotal;
|
|
39
|
+
job.startedAt = status.startedAt ?? job.startedAt;
|
|
40
|
+
job.updatedAt = status.lastUpdate ?? Date.now();
|
|
41
|
+
if (status.steps?.length) {
|
|
42
|
+
job.agents = status.steps.map((step) => step.agent);
|
|
43
|
+
}
|
|
44
|
+
job.sessionDir = status.sessionDir ?? job.sessionDir;
|
|
45
|
+
job.outputFile = status.outputFile ?? job.outputFile;
|
|
46
|
+
job.totalTokens = status.totalTokens ?? job.totalTokens;
|
|
47
|
+
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
48
|
+
} else {
|
|
49
|
+
job.status = job.status === "queued" ? "running" : job.status;
|
|
50
|
+
job.updatedAt = Date.now();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
55
|
+
}, POLL_INTERVAL_MS);
|
|
56
|
+
state.poller.unref?.();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleStarted = (data: unknown) => {
|
|
60
|
+
const info = data as {
|
|
61
|
+
id?: string;
|
|
62
|
+
asyncDir?: string;
|
|
63
|
+
agent?: string;
|
|
64
|
+
chain?: string[];
|
|
65
|
+
};
|
|
66
|
+
if (!info.id) return;
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
|
|
69
|
+
const agents = info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
|
|
70
|
+
state.asyncJobs.set(info.id, {
|
|
71
|
+
asyncId: info.id,
|
|
72
|
+
asyncDir,
|
|
73
|
+
status: "queued",
|
|
74
|
+
mode: info.chain ? "chain" : "single",
|
|
75
|
+
agents,
|
|
76
|
+
stepsTotal: agents?.length,
|
|
77
|
+
startedAt: now,
|
|
78
|
+
updatedAt: now,
|
|
79
|
+
});
|
|
80
|
+
if (state.lastUiContext) {
|
|
81
|
+
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
82
|
+
ensurePoller();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleComplete = (data: unknown) => {
|
|
87
|
+
const result = data as { id?: string; success?: boolean; asyncDir?: string };
|
|
88
|
+
const asyncId = result.id;
|
|
89
|
+
if (!asyncId) return;
|
|
90
|
+
const job = state.asyncJobs.get(asyncId);
|
|
91
|
+
if (job) {
|
|
92
|
+
job.status = result.success ? "complete" : "failed";
|
|
93
|
+
job.updatedAt = Date.now();
|
|
94
|
+
if (result.asyncDir) job.asyncDir = result.asyncDir;
|
|
95
|
+
}
|
|
96
|
+
if (state.lastUiContext) {
|
|
97
|
+
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
98
|
+
}
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
state.cleanupTimers.delete(asyncId);
|
|
101
|
+
state.asyncJobs.delete(asyncId);
|
|
102
|
+
if (state.lastUiContext) {
|
|
103
|
+
renderWidget(state.lastUiContext, Array.from(state.asyncJobs.values()));
|
|
104
|
+
}
|
|
105
|
+
}, 10000);
|
|
106
|
+
state.cleanupTimers.set(asyncId, timer);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const resetJobs = (ctx?: ExtensionContext) => {
|
|
110
|
+
for (const timer of state.cleanupTimers.values()) {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
state.cleanupTimers.clear();
|
|
114
|
+
state.asyncJobs.clear();
|
|
115
|
+
state.resultFileCoalescer.clear();
|
|
116
|
+
if (ctx?.hasUI) {
|
|
117
|
+
state.lastUiContext = ctx;
|
|
118
|
+
renderWidget(ctx, []);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return { ensurePoller, handleStarted, handleComplete, resetJobs };
|
|
123
|
+
}
|
package/chain-serializer.ts
CHANGED
|
@@ -1,34 +1,5 @@
|
|
|
1
1
|
import type { ChainConfig, ChainStepConfig } from "./agents.js";
|
|
2
|
-
|
|
3
|
-
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
4
|
-
const frontmatter: Record<string, string> = {};
|
|
5
|
-
const normalized = content.replace(/\r\n/g, "\n");
|
|
6
|
-
|
|
7
|
-
if (!normalized.startsWith("---")) {
|
|
8
|
-
return { frontmatter, body: normalized };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const endIndex = normalized.indexOf("\n---", 3);
|
|
12
|
-
if (endIndex === -1) {
|
|
13
|
-
return { frontmatter, body: normalized };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
17
|
-
const body = normalized.slice(endIndex + 4).trim();
|
|
18
|
-
|
|
19
|
-
for (const line of frontmatterBlock.split("\n")) {
|
|
20
|
-
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
21
|
-
if (match) {
|
|
22
|
-
let value = match[2].trim();
|
|
23
|
-
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
24
|
-
value = value.slice(1, -1);
|
|
25
|
-
}
|
|
26
|
-
frontmatter[match[1]] = value;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return { frontmatter, body };
|
|
31
|
-
}
|
|
2
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
32
3
|
|
|
33
4
|
function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
34
5
|
const lines = sectionBody.split("\n");
|
package/execution.ts
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import * as fs from "node:fs";
|
|
7
|
-
import * as os from "node:os";
|
|
8
|
-
import * as path from "node:path";
|
|
9
6
|
import type { Message } from "@mariozechner/pi-ai";
|
|
10
7
|
import type { AgentConfig } from "./agents.js";
|
|
11
8
|
import {
|
|
@@ -24,7 +21,6 @@ import {
|
|
|
24
21
|
getSubagentDepthEnv,
|
|
25
22
|
} from "./types.js";
|
|
26
23
|
import {
|
|
27
|
-
writePrompt,
|
|
28
24
|
getFinalOutput,
|
|
29
25
|
findLatestSessionFile,
|
|
30
26
|
detectSubagentError,
|
|
@@ -34,15 +30,7 @@ import {
|
|
|
34
30
|
import { buildSkillInjection, resolveSkills } from "./skills.js";
|
|
35
31
|
import { getPiSpawnCommand } from "./pi-spawn.js";
|
|
36
32
|
import { createJsonlWriter } from "./jsonl-writer.js";
|
|
37
|
-
|
|
38
|
-
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
39
|
-
|
|
40
|
-
export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined {
|
|
41
|
-
if (!model || !thinking || thinking === "off") return model;
|
|
42
|
-
const colonIdx = model.lastIndexOf(":");
|
|
43
|
-
if (colonIdx !== -1 && THINKING_LEVELS.includes(model.substring(colonIdx + 1))) return model;
|
|
44
|
-
return `${model}:${thinking}`;
|
|
45
|
-
}
|
|
33
|
+
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "./pi-args.js";
|
|
46
34
|
|
|
47
35
|
/**
|
|
48
36
|
* Run a subagent synchronously (blocking until complete)
|
|
@@ -67,85 +55,34 @@ export async function runSync(
|
|
|
67
55
|
};
|
|
68
56
|
}
|
|
69
57
|
|
|
70
|
-
const args = ["--mode", "json", "-p"];
|
|
71
58
|
const shareEnabled = options.share === true;
|
|
72
59
|
const sessionEnabled = Boolean(options.sessionDir) || shareEnabled;
|
|
73
|
-
if (!sessionEnabled) {
|
|
74
|
-
args.push("--no-session");
|
|
75
|
-
}
|
|
76
|
-
if (options.sessionDir) {
|
|
77
|
-
try {
|
|
78
|
-
fs.mkdirSync(options.sessionDir, { recursive: true });
|
|
79
|
-
} catch {}
|
|
80
|
-
args.push("--session-dir", options.sessionDir);
|
|
81
|
-
}
|
|
82
60
|
const effectiveModel = modelOverride ?? agent.model;
|
|
83
61
|
const modelArg = applyThinkingSuffix(effectiveModel, agent.thinking);
|
|
84
|
-
// Use --models (not --model) because pi CLI silently ignores --model
|
|
85
|
-
// without a companion --provider flag. --models resolves the provider
|
|
86
|
-
// automatically via resolveModelScope. See: #8
|
|
87
|
-
if (modelArg) args.push("--models", modelArg);
|
|
88
|
-
const toolExtensionPaths: string[] = [];
|
|
89
|
-
if (agent.tools?.length) {
|
|
90
|
-
const builtinTools: string[] = [];
|
|
91
|
-
for (const tool of agent.tools) {
|
|
92
|
-
if (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")) {
|
|
93
|
-
toolExtensionPaths.push(tool);
|
|
94
|
-
} else {
|
|
95
|
-
builtinTools.push(tool);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
if (builtinTools.length > 0) {
|
|
99
|
-
args.push("--tools", builtinTools.join(","));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (agent.extensions !== undefined) {
|
|
103
|
-
args.push("--no-extensions");
|
|
104
|
-
for (const extPath of agent.extensions) {
|
|
105
|
-
args.push("--extension", extPath);
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
for (const extPath of toolExtensionPaths) {
|
|
109
|
-
args.push("--extension", extPath);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
62
|
|
|
113
63
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
114
64
|
const { resolved: resolvedSkills, missing: missingSkills } = resolveSkills(skillNames, runtimeCwd);
|
|
115
65
|
|
|
116
|
-
// When explicit skills are specified (via options or agent config), disable
|
|
117
|
-
// pi's own skill discovery so the spawned process doesn't inject the full
|
|
118
|
-
// <available_skills> catalog. This mirrors how extensions are scoped above.
|
|
119
|
-
if (skillNames.length > 0) {
|
|
120
|
-
args.push("--no-skills");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
66
|
let systemPrompt = agent.systemPrompt?.trim() || "";
|
|
124
67
|
if (resolvedSkills.length > 0) {
|
|
125
68
|
const skillInjection = buildSkillInjection(resolvedSkills);
|
|
126
69
|
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
|
|
127
70
|
}
|
|
128
71
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const taskFilePath = path.join(tmpDir, "task.md");
|
|
144
|
-
fs.writeFileSync(taskFilePath, `Task: ${task}`, { mode: 0o600 });
|
|
145
|
-
args.push(`@${taskFilePath}`);
|
|
146
|
-
} else {
|
|
147
|
-
args.push(`Task: ${task}`);
|
|
148
|
-
}
|
|
72
|
+
const { args, env: sharedEnv, tempDir } = buildPiArgs({
|
|
73
|
+
baseArgs: ["--mode", "json", "-p"],
|
|
74
|
+
task,
|
|
75
|
+
sessionEnabled,
|
|
76
|
+
sessionDir: options.sessionDir,
|
|
77
|
+
model: effectiveModel,
|
|
78
|
+
thinking: agent.thinking,
|
|
79
|
+
tools: agent.tools,
|
|
80
|
+
extensions: agent.extensions,
|
|
81
|
+
skills: skillNames,
|
|
82
|
+
systemPrompt,
|
|
83
|
+
mcpDirectTools: agent.mcpDirectTools,
|
|
84
|
+
promptFileStem: agent.name,
|
|
85
|
+
});
|
|
149
86
|
|
|
150
87
|
const result: SingleResult = {
|
|
151
88
|
agent: agentName,
|
|
@@ -187,13 +124,7 @@ export async function runSync(
|
|
|
187
124
|
}
|
|
188
125
|
}
|
|
189
126
|
|
|
190
|
-
const spawnEnv = { ...process.env, ...getSubagentDepthEnv() };
|
|
191
|
-
const mcpDirect = agent.mcpDirectTools;
|
|
192
|
-
if (mcpDirect?.length) {
|
|
193
|
-
spawnEnv.MCP_DIRECT_TOOLS = mcpDirect.join(",");
|
|
194
|
-
} else {
|
|
195
|
-
spawnEnv.MCP_DIRECT_TOOLS = "__none__";
|
|
196
|
-
}
|
|
127
|
+
const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv() };
|
|
197
128
|
|
|
198
129
|
let closeJsonlWriter: (() => Promise<void>) | undefined;
|
|
199
130
|
const exitCode = await new Promise<number>((resolve) => {
|
|
@@ -379,7 +310,7 @@ export async function runSync(
|
|
|
379
310
|
} catch {}
|
|
380
311
|
}
|
|
381
312
|
|
|
382
|
-
|
|
313
|
+
cleanupTempDir(tempDir);
|
|
383
314
|
result.exitCode = exitCode;
|
|
384
315
|
|
|
385
316
|
if (exitCode === 0 && !result.error) {
|
package/frontmatter.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
|
2
|
+
const frontmatter: Record<string, string> = {};
|
|
3
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
4
|
+
|
|
5
|
+
if (!normalized.startsWith("---")) {
|
|
6
|
+
return { frontmatter, body: normalized };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
10
|
+
if (endIndex === -1) {
|
|
11
|
+
return { frontmatter, body: normalized };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
15
|
+
const body = normalized.slice(endIndex + 4).trim();
|
|
16
|
+
|
|
17
|
+
for (const line of frontmatterBlock.split("\n")) {
|
|
18
|
+
const match = line.match(/^([\w-]+):\s*(.*)$/);
|
|
19
|
+
if (match) {
|
|
20
|
+
let value = match[2].trim();
|
|
21
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
22
|
+
value = value.slice(1, -1);
|
|
23
|
+
}
|
|
24
|
+
frontmatter[match[1]] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return { frontmatter, body };
|
|
29
|
+
}
|