pi-monofold 0.1.0 → 0.2.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/README.md +36 -25
- package/index.ts +177 -40
- package/package.json +11 -2
- package/tsconfig.json +0 -11
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Pi Monofold
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/pi-monofold)
|
|
4
|
+
[](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml)
|
|
5
|
+
[](https://github.com/eiei114/pi-monofold/actions/workflows/auto-release.yml)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://github.com/eiei114/pi-monofold)
|
|
8
|
+
|
|
3
9
|
Pi Monofold (`pi-monofold`) is a Pi Coding Agent extension that folds multiple local repositories and folders into a guarded **Virtual Monorepo** for AI agents.
|
|
4
10
|
|
|
5
11
|
It keeps repositories physically separate, while giving Pi a lightweight manifest, routed writes, workspace-aware reads, guarded commands, and explicit git flows.
|
|
@@ -102,7 +108,7 @@ workspaces:
|
|
|
102
108
|
- name: "Product docs"
|
|
103
109
|
path: "../business"
|
|
104
110
|
tags: [business, markdown, planning]
|
|
105
|
-
capabilities: [read, writeDocs,
|
|
111
|
+
capabilities: [read, writeDocs, git]
|
|
106
112
|
contextFiles: [README.md, CONTEXT.md]
|
|
107
113
|
routes:
|
|
108
114
|
default: "Notes"
|
|
@@ -123,44 +129,49 @@ workspaces:
|
|
|
123
129
|
- name: "Application"
|
|
124
130
|
path: "../app"
|
|
125
131
|
tags: [development, app]
|
|
126
|
-
capabilities: [read, editCode, runCommands,
|
|
132
|
+
capabilities: [read, editCode, runCommands, git]
|
|
127
133
|
contextFiles: [README.md, AGENTS.md]
|
|
128
134
|
```
|
|
129
135
|
|
|
130
|
-
## Tools
|
|
131
|
-
|
|
132
|
-
- `monofold_list`: show manifest and git status summary.
|
|
133
|
-
- `monofold_read`: read files, search text, or show a tree inside readable workspaces.
|
|
134
|
-
- `monofold_write`: create routed Markdown outputs by `routeType`, `title`, and `body`.
|
|
135
|
-
- `monofold_git`: run guarded workspace git `status`, `commit`, or `push`.
|
|
136
|
-
- `monofold_init`: queue `/monofold:init`.
|
|
137
|
-
|
|
138
136
|
## Commands
|
|
139
137
|
|
|
140
|
-
-
|
|
141
|
-
|
|
142
|
-
- `/monofold:
|
|
143
|
-
- `/monofold:
|
|
144
|
-
- `/monofold:
|
|
145
|
-
- `/monofold:
|
|
146
|
-
- `/monofold:
|
|
147
|
-
- `/monofold:
|
|
148
|
-
- `/monofold:
|
|
138
|
+
Human-facing commands accept natural-language arguments and hand off interpretation to the Pi agent:
|
|
139
|
+
|
|
140
|
+
- `/monofold:explore [request]`: list, read, search, or inspect workspace trees.
|
|
141
|
+
- `/monofold:write [request]`: create routed Markdown outputs.
|
|
142
|
+
- `/monofold:config [request]`: add or change Workspaces and Project Workspaces.
|
|
143
|
+
- `/monofold:git [request]`: run git status, commit, push, or commit+push workflows.
|
|
144
|
+
- `/monofold:guide`: start an interactive guide for Explore, Write, Config, Git, init, and update flows.
|
|
145
|
+
- `/monofold:init`: create or update `.pi/monofold.yaml` with an interactive wizard.
|
|
146
|
+
- `/monofold:update [request]`: migrate/clean up legacy config and optionally hand a config-change request to the agent.
|
|
149
147
|
|
|
150
148
|
Examples:
|
|
151
149
|
|
|
152
150
|
```text
|
|
153
|
-
/monofold:
|
|
154
|
-
/monofold:
|
|
155
|
-
/monofold:
|
|
156
|
-
/monofold:
|
|
151
|
+
/monofold:explore show the project workspaces
|
|
152
|
+
/monofold:write write today's progress note for Pi Monofold
|
|
153
|
+
/monofold:config add 4_Project/NewApp as a Project Workspace under Obsidian Vault with tag project,newapp
|
|
154
|
+
/monofold:git commit and push the pi-monofold dev workspace
|
|
155
|
+
/monofold:guide
|
|
157
156
|
```
|
|
158
157
|
|
|
158
|
+
Fine-grained legacy commands such as `/monofold:list`, `/monofold:read`, `/monofold:search`, `/monofold:tree`, `/monofold:add`, `/monofold:project-add`, and underscore aliases are not part of the human command surface.
|
|
159
|
+
|
|
160
|
+
## Agent API
|
|
161
|
+
|
|
162
|
+
Pi agents use strict `monofold_*` tools behind the natural-language command surface:
|
|
163
|
+
|
|
164
|
+
- `monofold_list`: show manifest and git status summary.
|
|
165
|
+
- `monofold_read`: read files, search text, or show a tree inside readable workspaces.
|
|
166
|
+
- `monofold_write`: create routed Markdown outputs by `routeType`, `title`, and `body`.
|
|
167
|
+
- `monofold_git`: run guarded workspace git `status`, `commit`, `push`, or `commitPush`.
|
|
168
|
+
- `monofold_init`: queue `/monofold:init`.
|
|
169
|
+
|
|
159
170
|
Project Workspaces are listed under `workspaces[].projects`. Their `path` is relative to the parent workspace, `tags` are combined with parent tags, `capabilities` inherit unless explicitly replaced, and missing routes default to `default: "."` when the effective target has `writeDocs`.
|
|
160
171
|
|
|
161
172
|
## Updating configuration
|
|
162
173
|
|
|
163
|
-
`.pi/monofold.yaml` is the canonical config file. Legacy `.pi/monofold.yml` is still readable,
|
|
174
|
+
`.pi/monofold.yaml` is the canonical config file. Legacy `.pi/monofold.yml` is still readable. Intent commands try to migrate a legacy-only config automatically, show a notice, and continue with the legacy config if migration fails. `/monofold:update` migrates or cleans up legacy config, writes timestamped backups such as `.pi/monofold.yml.bak-20260524-153012`, and removes the legacy file after a successful write. If both `.yaml` and `.yml` exist, normal intent commands prefer canonical `.yaml`; `/monofold:update` handles legacy cleanup.
|
|
164
175
|
|
|
165
176
|
`/monofold:update` is a configuration migration command, not a Pi package updater. Use `pi update`, `pi update --extensions`, or `pi install ...@new-ref` for package updates.
|
|
166
177
|
|
|
@@ -178,4 +189,4 @@ When `.pi/monofold.yaml` or legacy `.pi/monofold.yml` exists, Pi Monofold guards
|
|
|
178
189
|
- Docs write: requires `writeDocs`.
|
|
179
190
|
- Code edit: requires `editCode`.
|
|
180
191
|
- Bash: requires workspace cwd and `runCommands`.
|
|
181
|
-
- Git commit/push via bash: blocked; use `monofold_git
|
|
192
|
+
- Git commit/push via bash: blocked; use `/monofold:git` or the `monofold_git` agent tool.
|
package/index.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { access, copyFile, mkdir, readFile, readdir, realpath, unlink, writeFile
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import YAML from "yaml";
|
|
7
7
|
|
|
8
|
-
type CapabilityTag = "read" | "writeDocs" | "editCode" | "runCommands" | "
|
|
8
|
+
type CapabilityTag = "read" | "writeDocs" | "editCode" | "runCommands" | "git";
|
|
9
|
+
type LegacyCapabilityTag = CapabilityTag | "gitCommit" | "gitPush";
|
|
10
|
+
type IntentCategory = "Explore" | "Write" | "Config" | "Git";
|
|
9
11
|
type RouteType = "default" | "prd" | "design" | "progress" | "issue" | "research" | "decision";
|
|
10
12
|
|
|
11
13
|
type RouteConfig = {
|
|
@@ -93,6 +95,9 @@ type ConfigMigrationPlan = {
|
|
|
93
95
|
targetRelativePath: string;
|
|
94
96
|
backupPath?: string;
|
|
95
97
|
backupRelativePath?: string;
|
|
98
|
+
cleanupLegacyPath?: string;
|
|
99
|
+
cleanupLegacyBackupPath?: string;
|
|
100
|
+
cleanupLegacyBackupRelativePath?: string;
|
|
96
101
|
normalizedText: string;
|
|
97
102
|
loaded: LoadedConfig;
|
|
98
103
|
actions: string[];
|
|
@@ -101,7 +106,8 @@ type ConfigMigrationPlan = {
|
|
|
101
106
|
const CONFIG_RELATIVE_PATH = path.join(".pi", "monofold.yaml");
|
|
102
107
|
const LEGACY_CONFIG_RELATIVE_PATH = path.join(".pi", "monofold.yml");
|
|
103
108
|
const ROUTE_TYPES: RouteType[] = ["default", "prd", "design", "progress", "issue", "research", "decision"];
|
|
104
|
-
const CAPABILITIES: CapabilityTag[] = ["read", "writeDocs", "editCode", "runCommands", "
|
|
109
|
+
const CAPABILITIES: CapabilityTag[] = ["read", "writeDocs", "editCode", "runCommands", "git"];
|
|
110
|
+
const LEGACY_CAPABILITIES: LegacyCapabilityTag[] = [...CAPABILITIES, "gitCommit", "gitPush"];
|
|
105
111
|
const DOC_EXTENSIONS = new Set([".md", ".mdx", ".txt", ".rst", ".adoc"]);
|
|
106
112
|
const CODE_EXTENSIONS = new Set([
|
|
107
113
|
".ts",
|
|
@@ -182,12 +188,14 @@ function asStringArray(label: string, value: unknown, required = true): string[]
|
|
|
182
188
|
|
|
183
189
|
function asCapabilityArray(value: unknown): CapabilityTag[] {
|
|
184
190
|
const items = asStringArray("capabilities", value);
|
|
191
|
+
const normalized: CapabilityTag[] = [];
|
|
185
192
|
for (const item of items) {
|
|
186
|
-
if (!
|
|
193
|
+
if (!LEGACY_CAPABILITIES.includes(item as LegacyCapabilityTag)) {
|
|
187
194
|
throw new Error(`Unknown capability: ${item}`);
|
|
188
195
|
}
|
|
196
|
+
normalized.push(item === "gitCommit" || item === "gitPush" ? "git" : (item as CapabilityTag));
|
|
189
197
|
}
|
|
190
|
-
return
|
|
198
|
+
return uniqueStrings(normalized) as CapabilityTag[];
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
function asOptionalCapabilityArray(value: unknown): CapabilityTag[] | undefined {
|
|
@@ -238,11 +246,16 @@ async function pathExists(targetPath: string): Promise<boolean> {
|
|
|
238
246
|
}
|
|
239
247
|
}
|
|
240
248
|
|
|
241
|
-
async function resolveConfigFile(
|
|
249
|
+
async function resolveConfigFile(
|
|
250
|
+
cwd: string,
|
|
251
|
+
allowMissing = false,
|
|
252
|
+
options: { preferCanonicalOnConflict?: boolean } = {},
|
|
253
|
+
): Promise<{ configPath: string; relativePath: string; kind: "canonical" | "legacy" | "missing" }> {
|
|
242
254
|
const canonicalPath = path.join(cwd, CONFIG_RELATIVE_PATH);
|
|
243
255
|
const legacyPath = path.join(cwd, LEGACY_CONFIG_RELATIVE_PATH);
|
|
244
256
|
const [hasCanonical, hasLegacy] = await Promise.all([pathExists(canonicalPath), pathExists(legacyPath)]);
|
|
245
257
|
if (hasCanonical && hasLegacy) {
|
|
258
|
+
if (options.preferCanonicalOnConflict) return { configPath: canonicalPath, relativePath: CONFIG_RELATIVE_PATH, kind: "canonical" };
|
|
246
259
|
throw new Error(`Configuration file conflict: both ${CONFIG_RELATIVE_PATH} and ${LEGACY_CONFIG_RELATIVE_PATH} exist. Remove one before continuing.`);
|
|
247
260
|
}
|
|
248
261
|
if (hasCanonical) return { configPath: canonicalPath, relativePath: CONFIG_RELATIVE_PATH, kind: "canonical" };
|
|
@@ -282,7 +295,7 @@ function runCommand(
|
|
|
282
295
|
}
|
|
283
296
|
|
|
284
297
|
async function loadConfig(cwd: string): Promise<LoadedConfig> {
|
|
285
|
-
const { configPath } = await resolveConfigFile(cwd);
|
|
298
|
+
const { configPath } = await resolveConfigFile(cwd, false, { preferCanonicalOnConflict: true });
|
|
286
299
|
const text = await readFile(configPath, "utf8");
|
|
287
300
|
const parsed = YAML.parse(text, { uniqueKeys: true }) as unknown;
|
|
288
301
|
return validateConfigObject(cwd, configPath, parsed);
|
|
@@ -436,6 +449,26 @@ function timestampSuffix(now = new Date()): string {
|
|
|
436
449
|
return now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z").replace("T", "-").replace(/Z$/, "");
|
|
437
450
|
}
|
|
438
451
|
|
|
452
|
+
function normalizeCapabilityValues(value: unknown): string[] | undefined {
|
|
453
|
+
if (value === undefined) return undefined;
|
|
454
|
+
return asCapabilityArray(value);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function normalizeConfigCapabilities(config: Record<string, unknown>): void {
|
|
458
|
+
const workspaces = Array.isArray(config.workspaces) ? (config.workspaces as unknown[]) : [];
|
|
459
|
+
for (const workspace of workspaces) {
|
|
460
|
+
if (!isRecord(workspace)) continue;
|
|
461
|
+
const capabilities = normalizeCapabilityValues(workspace.capabilities);
|
|
462
|
+
if (capabilities) workspace.capabilities = capabilities;
|
|
463
|
+
const projects = Array.isArray(workspace.projects) ? (workspace.projects as unknown[]) : [];
|
|
464
|
+
for (const project of projects) {
|
|
465
|
+
if (!isRecord(project)) continue;
|
|
466
|
+
const projectCapabilities = normalizeCapabilityValues(project.capabilities);
|
|
467
|
+
if (projectCapabilities) project.capabilities = projectCapabilities;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
439
472
|
function normalizeConfigForMigration(parsed: unknown): Record<string, unknown> {
|
|
440
473
|
if (!isRecord(parsed)) throw new Error("monofold config must be a YAML object");
|
|
441
474
|
assertKnownKeys("monofold config", parsed, ROOT_KEYS);
|
|
@@ -444,11 +477,15 @@ function normalizeConfigForMigration(parsed: unknown): Record<string, unknown> {
|
|
|
444
477
|
const normalized: Record<string, unknown> = { version: 1 };
|
|
445
478
|
if (parsed.defaults !== undefined) normalized.defaults = parsed.defaults;
|
|
446
479
|
normalized.workspaces = parsed.workspaces;
|
|
480
|
+
normalizeConfigCapabilities(normalized);
|
|
447
481
|
return normalized;
|
|
448
482
|
}
|
|
449
483
|
|
|
450
484
|
async function buildConfigMigrationPlan(cwd: string): Promise<ConfigMigrationPlan> {
|
|
451
|
-
const
|
|
485
|
+
const canonicalPath = path.join(cwd, CONFIG_RELATIVE_PATH);
|
|
486
|
+
const legacyPath = path.join(cwd, LEGACY_CONFIG_RELATIVE_PATH);
|
|
487
|
+
const [hasCanonical, hasLegacy] = await Promise.all([pathExists(canonicalPath), pathExists(legacyPath)]);
|
|
488
|
+
const source = await resolveConfigFile(cwd, false, { preferCanonicalOnConflict: true });
|
|
452
489
|
if (source.kind === "missing") throw new Error(`No Pi Monofold configuration found. Expected ${CONFIG_RELATIVE_PATH} or legacy ${LEGACY_CONFIG_RELATIVE_PATH}.`);
|
|
453
490
|
|
|
454
491
|
const originalText = await readFile(source.configPath, "utf8");
|
|
@@ -457,13 +494,16 @@ async function buildConfigMigrationPlan(cwd: string): Promise<ConfigMigrationPla
|
|
|
457
494
|
const targetPath = path.join(cwd, CONFIG_RELATIVE_PATH);
|
|
458
495
|
const normalizedText = YAML.stringify(normalized).trimEnd() + "\n";
|
|
459
496
|
const loaded = await validateConfigObject(cwd, targetPath, normalized);
|
|
460
|
-
const
|
|
497
|
+
const cleanupLegacy = hasCanonical && hasLegacy;
|
|
498
|
+
const changed = source.kind !== "canonical" || originalText !== normalizedText || cleanupLegacy;
|
|
461
499
|
const actions: string[] = [];
|
|
462
500
|
if (source.kind === "legacy") actions.push(`Move legacy config ${LEGACY_CONFIG_RELATIVE_PATH} to canonical ${CONFIG_RELATIVE_PATH}`);
|
|
463
501
|
if (originalText !== normalizedText) actions.push("Normalize YAML and ensure version: 1 is explicit");
|
|
464
|
-
if (source.kind === "legacy") actions.push(`Remove legacy config ${LEGACY_CONFIG_RELATIVE_PATH} after writing ${CONFIG_RELATIVE_PATH}`);
|
|
502
|
+
if (source.kind === "legacy" || cleanupLegacy) actions.push(`Remove legacy config ${LEGACY_CONFIG_RELATIVE_PATH} after writing ${CONFIG_RELATIVE_PATH}`);
|
|
465
503
|
if (!changed) actions.push(`Already current: ${CONFIG_RELATIVE_PATH} (version 1)`);
|
|
466
|
-
const
|
|
504
|
+
const suffix = timestampSuffix();
|
|
505
|
+
const backupPath = changed && (source.kind === "legacy" || originalText !== normalizedText) ? `${source.configPath}.bak-${suffix}` : undefined;
|
|
506
|
+
const cleanupLegacyBackupPath = cleanupLegacy ? `${legacyPath}.bak-${suffix}` : undefined;
|
|
467
507
|
return {
|
|
468
508
|
changed,
|
|
469
509
|
sourcePath: source.configPath,
|
|
@@ -473,6 +513,9 @@ async function buildConfigMigrationPlan(cwd: string): Promise<ConfigMigrationPla
|
|
|
473
513
|
targetRelativePath: CONFIG_RELATIVE_PATH,
|
|
474
514
|
backupPath,
|
|
475
515
|
backupRelativePath: backupPath ? normalizeSlashes(path.relative(cwd, backupPath)) : undefined,
|
|
516
|
+
cleanupLegacyPath: cleanupLegacy ? legacyPath : undefined,
|
|
517
|
+
cleanupLegacyBackupPath,
|
|
518
|
+
cleanupLegacyBackupRelativePath: cleanupLegacyBackupPath ? normalizeSlashes(path.relative(cwd, cleanupLegacyBackupPath)) : undefined,
|
|
476
519
|
normalizedText,
|
|
477
520
|
loaded,
|
|
478
521
|
actions,
|
|
@@ -481,11 +524,35 @@ async function buildConfigMigrationPlan(cwd: string): Promise<ConfigMigrationPla
|
|
|
481
524
|
|
|
482
525
|
async function applyConfigMigrationPlan(plan: ConfigMigrationPlan): Promise<void> {
|
|
483
526
|
if (!plan.changed) return;
|
|
484
|
-
if (!plan.backupPath) throw new Error("Migration backup path is required for changed configuration");
|
|
485
527
|
await mkdir(path.dirname(plan.targetPath), { recursive: true });
|
|
486
|
-
await copyFile(plan.sourcePath, plan.backupPath);
|
|
528
|
+
if (plan.backupPath) await copyFile(plan.sourcePath, plan.backupPath);
|
|
529
|
+
if (plan.cleanupLegacyPath && plan.cleanupLegacyBackupPath) await copyFile(plan.cleanupLegacyPath, plan.cleanupLegacyBackupPath);
|
|
487
530
|
await writeFile(plan.targetPath, plan.normalizedText, "utf8");
|
|
488
531
|
if (plan.sourceKind === "legacy") await unlink(plan.sourcePath);
|
|
532
|
+
if (plan.cleanupLegacyPath) await unlink(plan.cleanupLegacyPath);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function prepareIntentConfiguration(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
536
|
+
const canonicalPath = path.join(ctx.cwd, CONFIG_RELATIVE_PATH);
|
|
537
|
+
const legacyPath = path.join(ctx.cwd, LEGACY_CONFIG_RELATIVE_PATH);
|
|
538
|
+
const [hasCanonical, hasLegacy] = await Promise.all([pathExists(canonicalPath), pathExists(legacyPath)]);
|
|
539
|
+
if (!hasCanonical && !hasLegacy) {
|
|
540
|
+
ctx.ui.notify(`No ${CONFIG_RELATIVE_PATH} found. Queueing /monofold:init.`, "info");
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
if (!hasCanonical && hasLegacy) {
|
|
544
|
+
try {
|
|
545
|
+
const plan = await buildConfigMigrationPlan(ctx.cwd);
|
|
546
|
+
await applyConfigMigrationPlan(plan);
|
|
547
|
+
if (plan.changed) {
|
|
548
|
+
ctx.ui.notify(`Migrated ${LEGACY_CONFIG_RELATIVE_PATH} to ${CONFIG_RELATIVE_PATH}${plan.backupRelativePath ? ` (backup: ${plan.backupRelativePath})` : ""}.`, "info");
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
552
|
+
ctx.ui.notify(`Legacy migration failed; continuing with ${LEGACY_CONFIG_RELATIVE_PATH}: ${message}`, "error");
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return true;
|
|
489
556
|
}
|
|
490
557
|
|
|
491
558
|
function formatMigrationPlan(plan: ConfigMigrationPlan): string {
|
|
@@ -493,7 +560,8 @@ function formatMigrationPlan(plan: ConfigMigrationPlan): string {
|
|
|
493
560
|
return [
|
|
494
561
|
`Source: ${plan.sourceRelativePath}`,
|
|
495
562
|
`Target: ${plan.targetRelativePath}`,
|
|
496
|
-
`Backup: ${plan.backupRelativePath}`,
|
|
563
|
+
`Backup: ${plan.backupRelativePath ?? plan.cleanupLegacyBackupRelativePath ?? "none"}`,
|
|
564
|
+
...(plan.backupRelativePath && plan.cleanupLegacyBackupRelativePath ? [`Legacy backup: ${plan.cleanupLegacyBackupRelativePath}`] : []),
|
|
497
565
|
"",
|
|
498
566
|
"Actions:",
|
|
499
567
|
...plan.actions.map((action) => `- ${action}`),
|
|
@@ -503,13 +571,62 @@ function formatMigrationPlan(plan: ConfigMigrationPlan): string {
|
|
|
503
571
|
function buildConfigurationHandoffPrompt(request: string): string {
|
|
504
572
|
return [
|
|
505
573
|
"/monofold:update completed. Apply this requested Pi Monofold configuration change to `.pi/monofold.yaml`.",
|
|
506
|
-
"Edit the canonical config directly when needed, preserve valid YAML, then run
|
|
574
|
+
"Edit the canonical config directly when needed, preserve valid YAML, then run `monofold_list` as manifest validation.",
|
|
507
575
|
"",
|
|
508
576
|
"User request:",
|
|
509
577
|
request.trim(),
|
|
510
578
|
].join("\n");
|
|
511
579
|
}
|
|
512
580
|
|
|
581
|
+
function buildIntentHandoffPrompt(intent: IntentCategory, request: string): string {
|
|
582
|
+
const trimmed = request.trim();
|
|
583
|
+
const emptyInstruction =
|
|
584
|
+
intent === "Explore"
|
|
585
|
+
? "Ask what the user wants to list, read, search, or inspect."
|
|
586
|
+
: intent === "Write"
|
|
587
|
+
? "Ask what document to create and where it should be saved."
|
|
588
|
+
: intent === "Config"
|
|
589
|
+
? "Ask what Workspace or Project Workspace configuration change is needed."
|
|
590
|
+
: "Ask whether the user wants git status, commit, push, or commit+push.";
|
|
591
|
+
return [
|
|
592
|
+
`Pi Monofold ${intent} request. Interpret the user's natural-language input and continue as the agent.`,
|
|
593
|
+
"",
|
|
594
|
+
"Rules:",
|
|
595
|
+
"- Use strict `monofold_*` tools as the execution API; do not ask the user to write JSON or YAML unless needed.",
|
|
596
|
+
"- Select a Workspace Target automatically only when the manifest makes it unique; if multiple targets match, ask one clarifying question.",
|
|
597
|
+
"- Ask missing information incrementally, one question at a time.",
|
|
598
|
+
"- Explore intent: read/search/tree/list is read-only; execute immediately when target and path/query are clear.",
|
|
599
|
+
"- Write intent: infer route/title/body/filename when possible, but confirm Workspace, route, and filename before calling `monofold_write`.",
|
|
600
|
+
"- Config intent: edit `.pi/monofold.yaml` only after showing the YAML diff; validate afterward with `monofold_list`.",
|
|
601
|
+
"- Git intent: use `monofold_git`; for commit+push use action `commitPush` and one combined confirmation. If message is missing, propose one from the diff.",
|
|
602
|
+
"- If there is no `.pi/monofold.yaml`, guide the user to `/monofold:init`.",
|
|
603
|
+
"",
|
|
604
|
+
"Intent:",
|
|
605
|
+
intent,
|
|
606
|
+
"",
|
|
607
|
+
"User request:",
|
|
608
|
+
trimmed || `(empty input) ${emptyInstruction}`,
|
|
609
|
+
].join("\n");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function buildGuideHandoffPrompt(request: string): string {
|
|
613
|
+
return [
|
|
614
|
+
"Pi Monofold guide request. Start a conversational helper flow for Pi Monofold.",
|
|
615
|
+
"",
|
|
616
|
+
"Guide the user through Explore, Write, Config, Git, init, or update. Do not dump a static help page.",
|
|
617
|
+
"Ask one question at a time, then route to the appropriate intent behavior:",
|
|
618
|
+
"- Explore: list/read/search/tree workspaces.",
|
|
619
|
+
"- Write: routed Markdown output.",
|
|
620
|
+
"- Config: Workspace or Project Workspace configuration changes with YAML diff confirmation.",
|
|
621
|
+
"- Git: status/commit/push/commit+push via `monofold_git`.",
|
|
622
|
+
"- Init: queue or instruct `/monofold:init`.",
|
|
623
|
+
"- Update: run or instruct `/monofold:update` for migration/cleanup.",
|
|
624
|
+
"",
|
|
625
|
+
"Initial user request:",
|
|
626
|
+
request.trim() || "(empty input) Ask what they want to do with Pi Monofold.",
|
|
627
|
+
].join("\n");
|
|
628
|
+
}
|
|
629
|
+
|
|
513
630
|
function matchesTarget(workspace: ResolvedWorkspace, target: TargetInput): boolean {
|
|
514
631
|
if (target.targetId && workspace.targetId !== (target.targetId.startsWith("#") ? target.targetId : `#${target.targetId}`)) return false;
|
|
515
632
|
if (target.workspaceIndex !== undefined && workspace.index !== target.workspaceIndex) return false;
|
|
@@ -1028,9 +1145,9 @@ ${manifest}
|
|
|
1028
1145
|
pi.registerTool({
|
|
1029
1146
|
name: "monofold_git",
|
|
1030
1147
|
label: "Workspace Git",
|
|
1031
|
-
description: "Run guarded git status, commit, or
|
|
1148
|
+
description: "Run guarded git status, commit, push, or commitPush for one configured Git Workspace.",
|
|
1032
1149
|
parameters: Type.Object({
|
|
1033
|
-
action: Type.String({ description: "status, commit, or
|
|
1150
|
+
action: Type.String({ description: "status, commit, push, or commitPush" }),
|
|
1034
1151
|
message: Type.Optional(Type.String()),
|
|
1035
1152
|
targetTags: Type.Optional(Type.Array(Type.String())),
|
|
1036
1153
|
targetName: Type.Optional(Type.String()),
|
|
@@ -1038,7 +1155,7 @@ ${manifest}
|
|
|
1038
1155
|
workspaceName: Type.Optional(Type.String()),
|
|
1039
1156
|
}),
|
|
1040
1157
|
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
1041
|
-
const required: CapabilityTag[] = params.action === "
|
|
1158
|
+
const required: CapabilityTag[] = params.action === "status" ? ["read"] : ["git"];
|
|
1042
1159
|
const loaded = await loadConfig(ctx.cwd);
|
|
1043
1160
|
const workspace = await resolveWorkspace(ctx, loaded, {
|
|
1044
1161
|
targetTags: params.targetTags,
|
|
@@ -1050,18 +1167,35 @@ ${manifest}
|
|
|
1050
1167
|
const root = await gitRoot(workspace);
|
|
1051
1168
|
if (!root) throw new Error(`Not a Git Workspace: ${formatWorkspaceLabel(workspace)}`);
|
|
1052
1169
|
if (params.action === "status") {
|
|
1170
|
+
if (!workspace.capabilities.includes("read")) throw new Error(`Workspace lacks read capability: ${formatWorkspaceLabel(workspace)}`);
|
|
1053
1171
|
const result = await runCommand("git", ["-C", root, "status", "--short", "--branch"], { signal, timeout: 10000 });
|
|
1054
1172
|
return { content: [{ type: "text", text: result.stdout || "clean" }], details: { workspace } };
|
|
1055
1173
|
}
|
|
1056
|
-
if (
|
|
1174
|
+
if (!workspace.capabilities.includes("git")) throw new Error(`Workspace lacks git capability: ${formatWorkspaceLabel(workspace)}`);
|
|
1175
|
+
if (params.action === "commit" || params.action === "commitPush") {
|
|
1057
1176
|
const message = params.message ?? `Update ${workspace.name ?? (workspace.tags.join("-") || "workspace")}`;
|
|
1058
1177
|
const status = await runCommand("git", ["-C", root, "status", "--short"], { signal, timeout: 10000 });
|
|
1059
1178
|
const diffstat = await runCommand("git", ["-C", root, "diff", "--stat"], { signal, timeout: 10000 });
|
|
1060
1179
|
const scope = workspace.kind === "project" && root !== workspace.resolvedPath ? normalizeSlashes(path.relative(root, workspace.resolvedPath)) : ".";
|
|
1061
|
-
|
|
1062
|
-
if (
|
|
1180
|
+
let pushContext = "";
|
|
1181
|
+
if (params.action === "commitPush") {
|
|
1182
|
+
const branch = await runCommand("git", ["-C", root, "branch", "--show-current"], { signal, timeout: 10000 });
|
|
1183
|
+
const remote = await runCommand("git", ["-C", root, "remote", "-v"], { signal, timeout: 10000 });
|
|
1184
|
+
const log = await runCommand("git", ["-C", root, "log", "--oneline", "@{u}..HEAD"], {
|
|
1185
|
+
signal,
|
|
1186
|
+
timeout: 10000,
|
|
1187
|
+
allowExitCodes: [0, 128],
|
|
1188
|
+
});
|
|
1189
|
+
pushContext = `\n\nPush after commit:\nBranch: ${branch.stdout.trim()}\n\nRemote:\n${remote.stdout}\nCommits already ahead:\n${log.stdout || "none/unknown upstream"}`;
|
|
1190
|
+
}
|
|
1191
|
+
const ok = await confirm(ctx, params.action === "commitPush" ? "Workspace Commit + Push" : "Workspace Commit", `${formatWorkspaceLabel(workspace)}\n\nStatus (repo full):\n${status.stdout || "clean"}\n\nDiffstat (repo full):\n${diffstat.stdout || "none"}\n\nCommit scope:\n${scope}\n\nCommit message:\n${message}${pushContext}\n\n${params.action === "commitPush" ? "Stage scoped changes, commit, then push?" : "Stage scoped changes and commit?"}`);
|
|
1192
|
+
if (!ok) return { content: [{ type: "text", text: params.action === "commitPush" ? "Commit+push cancelled" : "Commit cancelled" }], details: { cancelled: true } };
|
|
1063
1193
|
await runCommand("git", ["-C", root, "add", "-A", "--", scope], { signal, timeout: 10000 });
|
|
1064
1194
|
const commit = await runCommand("git", ["-C", root, "commit", "-m", message], { signal, timeout: 30000 });
|
|
1195
|
+
if (params.action === "commitPush") {
|
|
1196
|
+
const push = await runCommand("git", ["-C", root, "push"], { signal, timeout: 60000 });
|
|
1197
|
+
return { content: [{ type: "text", text: [commit.stdout || commit.stderr, push.stdout || push.stderr].filter(Boolean).join("\n") }], details: { workspace, message } };
|
|
1198
|
+
}
|
|
1065
1199
|
return { content: [{ type: "text", text: commit.stdout || commit.stderr }], details: { workspace, message } };
|
|
1066
1200
|
}
|
|
1067
1201
|
if (params.action === "push") {
|
|
@@ -1206,7 +1340,7 @@ ${manifest}
|
|
|
1206
1340
|
try {
|
|
1207
1341
|
const parsed = parseCommandArgs(args);
|
|
1208
1342
|
const action = parsed.positional[0] ?? "status";
|
|
1209
|
-
const required: CapabilityTag[] = action === "
|
|
1343
|
+
const required: CapabilityTag[] = action === "status" ? ["read"] : ["git"];
|
|
1210
1344
|
const loaded = await loadConfig(ctx.cwd);
|
|
1211
1345
|
const workspace = await resolveWorkspace(ctx, loaded, commandTarget(parsed.flags, required));
|
|
1212
1346
|
const root = await gitRoot(workspace);
|
|
@@ -1322,6 +1456,7 @@ ${manifest}
|
|
|
1322
1456
|
changed: plan.changed,
|
|
1323
1457
|
configPath: CONFIG_RELATIVE_PATH,
|
|
1324
1458
|
backupPath: plan.backupRelativePath,
|
|
1459
|
+
legacyBackupPath: plan.cleanupLegacyBackupRelativePath,
|
|
1325
1460
|
});
|
|
1326
1461
|
|
|
1327
1462
|
let request = args.trim();
|
|
@@ -1336,20 +1471,26 @@ ${manifest}
|
|
|
1336
1471
|
}
|
|
1337
1472
|
};
|
|
1338
1473
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1474
|
+
const intentCommand = (intent: IntentCategory) => async (args: string, ctx: ExtensionCommandContext) => {
|
|
1475
|
+
const prepared = await prepareIntentConfiguration(ctx);
|
|
1476
|
+
if (!prepared) {
|
|
1477
|
+
pi.sendUserMessage("/monofold:init", { deliverAs: "followUp" });
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
pi.sendUserMessage(buildIntentHandoffPrompt(intent, args), { deliverAs: "followUp" });
|
|
1481
|
+
sendCommandOutput(pi, `monofold:${intent.toLowerCase()}`, `Queued ${intent} handoff.`, { intent, request: args.trim() });
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
const guideCommand = async (args: string, _ctx: ExtensionCommandContext) => {
|
|
1485
|
+
pi.sendUserMessage(buildGuideHandoffPrompt(args), { deliverAs: "followUp" });
|
|
1486
|
+
sendCommandOutput(pi, "monofold:guide", "Queued Monofold guide.", { request: args.trim() });
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
pi.registerCommand("monofold:explore", { description: "Explore configured workspaces via natural-language handoff", handler: intentCommand("Explore") });
|
|
1490
|
+
pi.registerCommand("monofold:write", { description: "Create routed Markdown via natural-language handoff", handler: intentCommand("Write") });
|
|
1491
|
+
pi.registerCommand("monofold:config", { description: "Change Workspace configuration via natural-language handoff", handler: intentCommand("Config") });
|
|
1492
|
+
pi.registerCommand("monofold:git", { description: "Run workspace git workflows via natural-language handoff", handler: intentCommand("Git") });
|
|
1493
|
+
pi.registerCommand("monofold:guide", { description: "Conversational guide for Pi Monofold workflows", handler: guideCommand });
|
|
1353
1494
|
pi.registerCommand("monofold:update", { description: `Migrate and validate ${CONFIG_RELATIVE_PATH}`, handler: updateCommand });
|
|
1354
1495
|
|
|
1355
1496
|
const initCommand = async (_args: string, ctx: ExtensionCommandContext) => {
|
|
@@ -1395,7 +1536,7 @@ ${manifest}
|
|
|
1395
1536
|
const name = await ctx.ui.input("Optional workspace name", "");
|
|
1396
1537
|
const tagsInput = await ctx.ui.input("Tags comma-separated", "business,markdown");
|
|
1397
1538
|
if (!tagsInput) return;
|
|
1398
|
-
const capsInput = await ctx.ui.input("Capabilities comma-separated", "read,writeDocs,
|
|
1539
|
+
const capsInput = await ctx.ui.input("Capabilities comma-separated", "read,writeDocs,git");
|
|
1399
1540
|
if (!capsInput) return;
|
|
1400
1541
|
const capabilities = capsInput.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1401
1542
|
const routePath = capabilities.includes("writeDocs") ? await ctx.ui.input("Default document route", "Notes") : undefined;
|
|
@@ -1422,10 +1563,6 @@ ${manifest}
|
|
|
1422
1563
|
description: `Create or update ${CONFIG_RELATIVE_PATH} with an interactive wizard`,
|
|
1423
1564
|
handler: initCommand,
|
|
1424
1565
|
});
|
|
1425
|
-
pi.registerCommand("monofold_init", {
|
|
1426
|
-
description: "Alias for /monofold:init",
|
|
1427
|
-
handler: initCommand,
|
|
1428
|
-
});
|
|
1429
1566
|
|
|
1430
1567
|
pi.registerTool({
|
|
1431
1568
|
name: "monofold_init",
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"scripts": {
|
|
3
|
-
"typecheck": "tsc --noEmit"
|
|
3
|
+
"typecheck": "tsc --noEmit",
|
|
4
|
+
"check": "npm run typecheck && npm pack --dry-run"
|
|
4
5
|
},
|
|
5
6
|
"peerDependencies": {
|
|
6
7
|
"typebox": "*",
|
|
@@ -9,12 +10,17 @@
|
|
|
9
10
|
},
|
|
10
11
|
"description": "Pi extension that folds multiple repositories and folders into a guarded virtual monorepo for AI agents.",
|
|
11
12
|
"type": "module",
|
|
12
|
-
"version": "0.
|
|
13
|
+
"version": "0.2.0",
|
|
13
14
|
"pi": {
|
|
14
15
|
"extensions": [
|
|
15
16
|
"./index.ts"
|
|
16
17
|
]
|
|
17
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"index.ts"
|
|
23
|
+
],
|
|
18
24
|
"name": "pi-monofold",
|
|
19
25
|
"devDependencies": {
|
|
20
26
|
"typescript": "^6.0.3",
|
|
@@ -41,5 +47,8 @@
|
|
|
41
47
|
"url": "https://github.com/eiei114/pi-monofold/issues"
|
|
42
48
|
},
|
|
43
49
|
"homepage": "https://github.com/eiei114/pi-monofold#readme",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
44
53
|
"license": "MIT"
|
|
45
54
|
}
|