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.
Files changed (4) hide show
  1. package/README.md +36 -25
  2. package/index.ts +177 -40
  3. package/package.json +11 -2
  4. package/tsconfig.json +0 -11
package/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Pi Monofold
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/pi-monofold?color=cb3837&logo=npm)](https://www.npmjs.com/package/pi-monofold)
4
+ [![Publish to npm](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/publish.yml)
5
+ [![Auto Release](https://github.com/eiei114/pi-monofold/actions/workflows/auto-release.yml/badge.svg)](https://github.com/eiei114/pi-monofold/actions/workflows/auto-release.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
7
+ [![Pi Package](https://img.shields.io/badge/Pi-package-6f42c1)](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, gitCommit]
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, gitCommit, gitPush]
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
- - `/monofold:list` or `/monofold_list`: show manifest and git status summary.
141
- - `/monofold:add <path> --name "Name" --tags tag1,tag2 --capabilities read,editCode,runCommands,gitCommit`: add a workspace to `.pi/monofold.yaml`.
142
- - `/monofold:project-add <path> --parent "Name" --tags project,slug`: add a Project Workspace under a parent workspace.
143
- - `/monofold:update [natural language request]`: migrate legacy config to `.pi/monofold.yaml`, normalize YAML, validate the manifest, and optionally hand a config-change request to the agent.
144
- - `/monofold:read file <path> --target #0.1`: read a file from a workspace or project target.
145
- - `/monofold:tree [path] --target #0.1 --depth 2`: show a target tree.
146
- - `/monofold:search <query> --target #0.1`: search a target.
147
- - `/monofold:write --route progress --title "Title" --body "Markdown body"`: write routed Markdown.
148
- - `/monofold:git status|commit|push --target #0.1`: run guarded target git.
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:add C:/Projects/app --name "Application" --tags development,app --capabilities read,editCode,runCommands,gitCommit --context README.md,AGENTS.md
154
- /monofold:add ../business --name "Product Docs" --tags business,docs --capabilities read,writeDocs,gitCommit --routes default=Notes,progress=Progress,research=Research
155
- /monofold:project-add Projects/Launch --parent "Product Docs" --tags project,launch --routes default=.,progress=Progress
156
- /monofold:update rename the Product Docs workspace to Business Notes and add tag docs
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, but `/monofold:update` migrates it to `.pi/monofold.yaml`, writes a timestamped backup such as `.pi/monofold.yml.bak-20260524-153012`, and removes the legacy file after a successful write. If both `.yaml` and `.yml` exist, Pi Monofold stops with a conflict error so you can choose the correct file manually.
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" | "gitCommit" | "gitPush";
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", "gitCommit", "gitPush"];
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 (!CAPABILITIES.includes(item as CapabilityTag)) {
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 items as CapabilityTag[];
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(cwd: string, allowMissing = false): Promise<{ configPath: string; relativePath: string; kind: "canonical" | "legacy" | "missing" }> {
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 source = await resolveConfigFile(cwd);
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 changed = source.kind !== "canonical" || originalText !== normalizedText;
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 backupPath = changed ? `${source.configPath}.bak-${timestampSuffix()}` : undefined;
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 `/monofold:list` as manifest validation.",
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 push for one configured Git Workspace.",
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 push" }),
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 === "push" ? ["gitPush"] : params.action === "commit" ? ["gitCommit"] : [];
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 (params.action === "commit") {
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
- const ok = await confirm(ctx, "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}\n\nStage scoped changes and commit?`);
1062
- if (!ok) return { content: [{ type: "text", text: "Commit cancelled" }], details: { cancelled: true } };
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 === "push" ? ["gitPush"] : action === "commit" ? ["gitCommit"] : [];
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
- pi.registerCommand("monofold:list", { description: "List configured Pi Monofold workspaces", handler: listCommand });
1340
- pi.registerCommand("monofold_list", { description: "Alias for /monofold:list", handler: listCommand });
1341
- pi.registerCommand("monofold:tree", { description: "Show a tree for a configured workspace", handler: readCommand });
1342
- pi.registerCommand("monofold:read", { description: "Read, tree, or search a configured workspace", handler: readCommand });
1343
- pi.registerCommand("monofold_read", { description: "Alias for /monofold:read", handler: readCommand });
1344
- pi.registerCommand("monofold:search", { description: "Search a configured workspace", handler: (args, ctx) => readCommand(`search ${args}`, ctx) });
1345
- pi.registerCommand("monofold:write", { description: "Write a routed Markdown document", handler: writeCommand });
1346
- pi.registerCommand("monofold_write", { description: "Alias for /monofold:write", handler: writeCommand });
1347
- pi.registerCommand("monofold:git", { description: "Run guarded workspace git status, commit, or push", handler: gitCommand });
1348
- pi.registerCommand("monofold_git", { description: "Alias for /monofold:git", handler: gitCommand });
1349
- pi.registerCommand("monofold:add", { description: `Add a workspace to ${CONFIG_RELATIVE_PATH}`, handler: addCommand });
1350
- pi.registerCommand("monofold_add", { description: "Alias for /monofold:add", handler: addCommand });
1351
- pi.registerCommand("monofold:project-add", { description: "Add a project workspace under a parent workspace", handler: projectAddCommand });
1352
- pi.registerCommand("monofold_project_add", { description: "Alias for /monofold:project-add", handler: projectAddCommand });
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,gitCommit");
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.1.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
  }
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2022",
4
- "module": "NodeNext",
5
- "moduleResolution": "NodeNext",
6
- "skipLibCheck": true,
7
- "strict": true,
8
- "noEmit": true
9
- },
10
- "include": ["index.ts"]
11
- }