@agimon-ai/doompi 0.0.1-alpha.72 → 0.0.1-alpha.74

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 (58) hide show
  1. package/dist/builders/cli/contextCatalog.cjs +15 -3
  2. package/dist/builders/cli/contextCatalog.cjs.map +1 -1
  3. package/dist/builders/cli/contextCatalog.mjs +15 -3
  4. package/dist/builders/cli/contextCatalog.mjs.map +1 -1
  5. package/dist/builders/cli/projectSettings.cjs +1 -1
  6. package/dist/builders/cli/projectSettings.cjs.map +1 -1
  7. package/dist/builders/cli/projectSettings.mjs +1 -1
  8. package/dist/builders/cli/projectSettings.mjs.map +1 -1
  9. package/dist/builders/server/runtime.cjs +60 -5
  10. package/dist/builders/server/runtime.cjs.map +1 -1
  11. package/dist/builders/server/runtime.mjs +61 -6
  12. package/dist/builders/server/runtime.mjs.map +1 -1
  13. package/dist/builders/web/bundle.cjs +1 -0
  14. package/dist/builders/web/bundle.cjs.map +1 -1
  15. package/dist/builders/web/bundle.mjs +1 -0
  16. package/dist/builders/web/bundle.mjs.map +1 -1
  17. package/dist/builders/web/webPluginGenerate.cjs +12 -5
  18. package/dist/builders/web/webPluginGenerate.cjs.map +1 -1
  19. package/dist/builders/web/webPluginGenerate.d.cts.map +1 -1
  20. package/dist/builders/web/webPluginGenerate.d.mts.map +1 -1
  21. package/dist/builders/web/webPluginGenerate.mjs +12 -5
  22. package/dist/builders/web/webPluginGenerate.mjs.map +1 -1
  23. package/dist/builders/web/webPluginVite.cjs +9 -0
  24. package/dist/builders/web/webPluginVite.cjs.map +1 -1
  25. package/dist/builders/web/webPluginVite.d.cts +9 -0
  26. package/dist/builders/web/webPluginVite.d.cts.map +1 -1
  27. package/dist/builders/web/webPluginVite.d.mts +9 -0
  28. package/dist/builders/web/webPluginVite.d.mts.map +1 -1
  29. package/dist/builders/web/webPluginVite.mjs +9 -0
  30. package/dist/builders/web/webPluginVite.mjs.map +1 -1
  31. package/dist/cli/server/run.cjs +2 -0
  32. package/dist/cli/server/run.cjs.map +1 -1
  33. package/dist/cli/server/run.mjs +2 -0
  34. package/dist/cli/server/run.mjs.map +1 -1
  35. package/dist/compiler/index.cjs +5 -5
  36. package/dist/compiler/index.cjs.map +1 -1
  37. package/dist/compiler/index.mjs +5 -5
  38. package/dist/compiler/index.mjs.map +1 -1
  39. package/dist/extensions/context-catalog.cjs +35 -4
  40. package/dist/extensions/context-catalog.cjs.map +1 -1
  41. package/dist/extensions/context-catalog.mjs +35 -4
  42. package/dist/extensions/context-catalog.mjs.map +1 -1
  43. package/dist/extensions/terminal-child-session.cjs +18 -0
  44. package/dist/extensions/terminal-child-session.cjs.map +1 -1
  45. package/dist/extensions/terminal-child-session.d.cts.map +1 -1
  46. package/dist/extensions/terminal-child-session.d.mts.map +1 -1
  47. package/dist/extensions/terminal-child-session.mjs +18 -0
  48. package/dist/extensions/terminal-child-session.mjs.map +1 -1
  49. package/docs/server/api-export.md +10 -0
  50. package/docs/server/api.md +5 -5
  51. package/docs/server/getting-started.md +4 -4
  52. package/docs/server/index.md +6 -6
  53. package/docs/server/ipc.md +28 -14
  54. package/docs/server/lifecycle.md +5 -5
  55. package/docs/server/security.md +4 -4
  56. package/package.json +25 -25
  57. package/src/prompts/doompi-author-extension/SKILL.md +4 -3
  58. package/src/prompts/doompi-author-extension/references/extension-contract.md +26 -26
@@ -57,6 +57,11 @@ function createContextPublisher(pi, cordis, options = {}) {
57
57
  })).groups);
58
58
  } catch {}
59
59
  const countTokens = await (0, _agimon_ai_doompi_skill_catalog.counter)();
60
+ const prompt = options.readSystemPrompt?.();
61
+ const promptCost = prompt === void 0 ? void 0 : {
62
+ tokens: countTokens(prompt.text),
63
+ stage: prompt.stage
64
+ };
60
65
  const projection = (0, _agimon_ai_doompi_core_context_projection.projectContext)({
61
66
  revision: revision + 1,
62
67
  majorMode: harness.majorMode,
@@ -68,11 +73,13 @@ function createContextPublisher(pi, cordis, options = {}) {
68
73
  sources,
69
74
  skills,
70
75
  attribution: attributionFor(repoRoot, harness.domains, harness.pluginDirectories),
71
- countTokens
76
+ countTokens,
77
+ ...promptCost === void 0 ? {} : { systemPrompt: promptCost }
72
78
  });
73
79
  const serialized = JSON.stringify({
74
80
  ...projection,
75
- revision: 0
81
+ revision: 0,
82
+ promptText: prompt?.text
76
83
  });
77
84
  if (serialized === published || disposed) return;
78
85
  published = serialized;
@@ -83,7 +90,12 @@ function createContextPublisher(pi, cordis, options = {}) {
83
90
  options.writeDetail(sessionId, revision, (0, _agimon_ai_doompi_core_context_detail.buildContextDetail)({
84
91
  sources,
85
92
  skills,
86
- countTokens
93
+ countTokens,
94
+ ...prompt === void 0 || promptCost === void 0 ? {} : { systemPrompt: {
95
+ text: prompt.text,
96
+ tokens: promptCost.tokens,
97
+ stage: prompt.stage
98
+ } }
87
99
  }));
88
100
  }
89
101
  pi.appendEntry(_agimon_ai_doompi_core_context_projection.DOOM_CONTEXT_ENTRY_TYPE, {
@@ -1 +1 @@
1
- {"version":3,"file":"contextCatalog.cjs","names":["getHarnessState","resolvePluginEntries","readDoomMcpStatus","buildToolSources","extensionName","extensionPackageName","extensionToolSource","harnessRoot","buildSkillCatalog","readDoomSkillSourcesService","counter","projectContext","buildContextDetail","DOOM_CONTEXT_ENTRY_TYPE"],"sources":["../../../src/builders/cli/contextCatalog.ts"],"sourcesContent":["import { resolvePluginEntries } from '@agimon-ai/doompi-config/domains';\nimport { getHarnessState, harnessRoot } from '@agimon-ai/doompi-config/harnessStore';\nimport type { PackageAttribution } from '@agimon-ai/doompi-config/types';\nimport { buildContextDetail } from '@agimon-ai/doompi-core/context-detail';\nimport { DOOM_CONTEXT_ENTRY_TYPE, projectContext } from '@agimon-ai/doompi-core/context-projection';\nimport { readDoomMcpStatus } from '@agimon-ai/doompi-core/mcp-status';\nimport { readDoomSkillSourcesService } from '@agimon-ai/doompi-core/skills';\nimport type { ContextItemDetail } from '@agimon-ai/doompi-core/types-context-api';\nimport { buildSkillCatalog, counter, type SkillEntry } from '@agimon-ai/doompi-skill/catalog';\nimport { extensionName, extensionPackageName, extensionToolSource } from '@agimon-ai/doompi-ui/extensionName';\nimport { buildToolSources } from '@agimon-ai/doompi-ui/toolInventory';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\n\n/**\n * Publishes what the session is composed of, and what it costs.\n *\n * Read at publish time rather than accumulated, because the answer changes for\n * reasons this module does not see: a reconnected MCP server, a domain switch,\n * a plan-mode tool swap. Reading the live inventory is cheap next to being\n * wrong about it.\n */\nexport interface ContextPublisher {\n /** Never rejects: callers fire this and forget it. */\n publish: () => Promise<void>;\n dispose: () => void;\n}\n\n/** The active minor modes at publish time, read rather than remembered. */\nexport type ReadMinorModes = () => readonly { readonly id: string; readonly label: string }[];\n\nexport interface ContextPublisherOptions {\n readMinorModes?: ReadMinorModes;\n /**\n * The session the detail file is keyed by, read at publish time.\n *\n * Undefined until a session is bound, and undefined in a host that has no\n * session at all; the projection is still journaled either way, and only the\n * click-through detail goes unwritten.\n */\n readSessionId?: () => string | undefined;\n /**\n * Where the click-through detail is left for the session API to find.\n *\n * Injected because writing it is filesystem work and this is not the layer\n * that does filesystem work. A host that supplies neither still gets the\n * projection; only the detail behind a row goes unpublished.\n */\n writeDetail?: (sessionId: string, revision: number, items: readonly ContextItemDetail[]) => void;\n removeDetail?: (sessionId: string) => void;\n}\n\n/** Skills sit under owners, which is one level deeper than a flat list. */\nfunction flattenSkills(groups: Awaited<ReturnType<typeof buildSkillCatalog>>['groups']): SkillEntry[] {\n return groups.flatMap((group) => group.owners.flatMap((owner) => owner.skills));\n}\n\n/**\n * Package and plugin names to the mode that admitted them.\n *\n * Two halves meet here. Layer packages come from the harness, recorded when the\n * composition resolved. Domain plugins are re-resolved, because only the plugin\n * entry knows the domain that carried it.\n */\nfunction attributionFor(repoRoot: string, domains: readonly string[], pluginDirectories: readonly string[]) {\n const harness = getHarnessState();\n const attribution: Record<string, PackageAttribution> = { ...harness.packageAttribution };\n try {\n for (const entry of resolvePluginEntries(repoRoot, [...domains], [...pluginDirectories])) {\n if (entry.name && entry.domain) attribution[entry.name] = { kind: 'domain', mode: entry.domain };\n }\n } catch {\n // A domain that no longer resolves must not cost the reader the tool list.\n // Anything unmatched simply lands under core.\n }\n return attribution;\n}\n\nexport function createContextPublisher(\n pi: ExtensionAPI,\n cordis: Context,\n options: ContextPublisherOptions = {},\n): ContextPublisher {\n let published: string | undefined;\n let revision = 0;\n let disposed = false;\n /** Removed on disposal, so a session leaves nothing behind in the store. */\n let detailSessionId: string | undefined;\n\n const build = async (): Promise<void> => {\n if (disposed) return;\n const harness = getHarnessState();\n const mcpServers = readDoomMcpStatus(cordis)?.getSnapshot().servers;\n const sources = buildToolSources({\n tools: pi.getAllTools(),\n activeTools: pi.getActiveTools(),\n ...(mcpServers ? { mcpServers } : {}),\n resolveExtensionName: extensionName,\n resolveExtensionPackageName: extensionPackageName,\n resolveExtensionToolSource: (toolName) => extensionToolSource(pi, toolName),\n });\n\n const repoRoot = harnessRoot(harness);\n let skills: SkillEntry[] = [];\n try {\n const catalog = await buildSkillCatalog({\n repoRoot,\n activeSkillDirectories: harness.skillDirectories,\n extensionSources: readDoomSkillSourcesService(cordis)?.list() ?? [],\n });\n skills = flattenSkills(catalog.groups);\n } catch {\n // Tools are the larger cost and are already in hand; a skill walk that\n // fails should not take the whole figure down with it.\n }\n\n const countTokens = await counter();\n const projection = projectContext({\n revision: revision + 1,\n majorMode: harness.majorMode,\n groups: (options.readMinorModes?.() ?? []).map((mode) => ({ ...mode, kind: 'minor' as const })),\n domains: harness.domains,\n sources,\n skills,\n attribution: attributionFor(repoRoot, harness.domains, harness.pluginDirectories),\n countTokens,\n });\n\n // Revision is compared out, so a republish that changed nothing is silent\n // rather than a new journal entry saying the same thing.\n const serialized = JSON.stringify({ ...projection, revision: 0 });\n if (serialized === published || disposed) return;\n published = serialized;\n revision += 1;\n // The detail lands before the entry that invites a reader to ask for it, so\n // a click that follows the panel's update cannot outrun the file behind it.\n const sessionId = options.readSessionId?.();\n if (sessionId !== undefined && sessionId !== '' && options.writeDetail !== undefined) {\n detailSessionId = sessionId;\n options.writeDetail(sessionId, revision, buildContextDetail({ sources, skills, countTokens }));\n }\n pi.appendEntry(DOOM_CONTEXT_ENTRY_TYPE, { ...projection, revision });\n };\n\n /**\n * Reporting the composition is a convenience, so it fails quietly.\n *\n * Callers publish without awaiting, and an escaping rejection would surface\n * as an unhandled error in a live session. A panel that cannot say what the\n * toolbox costs is a far smaller problem than that.\n */\n const publish = async (): Promise<void> => {\n try {\n await build();\n } catch {\n // Left unreported on purpose: the next composition change tries again.\n }\n };\n\n return {\n publish,\n dispose: () => {\n disposed = true;\n if (detailSessionId !== undefined) options.removeDetail?.(detailSessionId);\n detailSessionId = undefined;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;AAqDA,SAAS,cAAc,QAA+E;CACpG,OAAO,OAAO,SAAS,UAAU,MAAM,OAAO,SAAS,UAAU,MAAM,MAAM,CAAC;AAChF;;;;;;;;AASA,SAAS,eAAe,UAAkB,SAA4B,mBAAsC;CAE1G,MAAM,cAAkD,EAAE,IAAA,GAD1CA,sCAAAA,gBAAAA,CACmD,CAAC,CAAC,mBAAmB;CACxF,IAAI;EACF,KAAK,MAAM,UAAA,GAASC,iCAAAA,qBAAAA,CAAqB,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,GAAG,iBAAiB,CAAC,GACrF,IAAI,MAAM,QAAQ,MAAM,QAAQ,YAAY,MAAM,QAAQ;GAAE,MAAM;GAAU,MAAM,MAAM;EAAO;CAEnG,QAAQ,CAGR;CACA,OAAO;AACT;AAEA,SAAgB,uBACd,IACA,QACA,UAAmC,CAAC,GAClB;CAClB,IAAI;CACJ,IAAI,WAAW;CACf,IAAI,WAAW;;CAEf,IAAI;CAEJ,MAAM,QAAQ,YAA2B;EACvC,IAAI,UAAU;EACd,MAAM,WAAA,GAAUD,sCAAAA,gBAAAA,CAAgB;EAChC,MAAM,cAAA,GAAaE,kCAAAA,kBAAAA,CAAkB,MAAM,CAAC,EAAE,YAAY,CAAC,CAAC;EAC5D,MAAM,WAAA,GAAUC,mCAAAA,iBAAAA,CAAiB;GAC/B,OAAO,GAAG,YAAY;GACtB,aAAa,GAAG,eAAe;GAC/B,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;GACnC,sBAAsBC,mCAAAA;GACtB,6BAA6BC,mCAAAA;GAC7B,6BAA6B,cAAA,GAAaC,mCAAAA,oBAAAA,CAAoB,IAAI,QAAQ;EAC5E,CAAC;EAED,MAAM,YAAA,GAAWC,sCAAAA,YAAAA,CAAY,OAAO;EACpC,IAAI,SAAuB,CAAC;EAC5B,IAAI;GAMF,SAAS,eAAc,OAAA,GALDC,gCAAAA,kBAAAA,CAAkB;IACtC;IACA,wBAAwB,QAAQ;IAChC,mBAAA,GAAkBC,8BAAAA,4BAAAA,CAA4B,MAAM,CAAC,EAAE,KAAK,KAAK,CAAC;GACpE,CAAC,EAAA,CAC8B,MAAM;EACvC,QAAQ,CAGR;EAEA,MAAM,cAAc,OAAA,GAAMC,gCAAAA,QAAAA,CAAQ;EAClC,MAAM,cAAA,GAAaC,0CAAAA,eAAAA,CAAe;GAChC,UAAU,WAAW;GACrB,WAAW,QAAQ;GACnB,SAAS,QAAQ,iBAAiB,KAAK,CAAC,EAAA,CAAG,KAAK,UAAU;IAAE,GAAG;IAAM,MAAM;GAAiB,EAAE;GAC9F,SAAS,QAAQ;GACjB;GACA;GACA,aAAa,eAAe,UAAU,QAAQ,SAAS,QAAQ,iBAAiB;GAChF;EACF,CAAC;EAID,MAAM,aAAa,KAAK,UAAU;GAAE,GAAG;GAAY,UAAU;EAAE,CAAC;EAChE,IAAI,eAAe,aAAa,UAAU;EAC1C,YAAY;EACZ,YAAY;EAGZ,MAAM,YAAY,QAAQ,gBAAgB;EAC1C,IAAI,cAAc,KAAA,KAAa,cAAc,MAAM,QAAQ,gBAAgB,KAAA,GAAW;GACpF,kBAAkB;GAClB,QAAQ,YAAY,WAAW,WAAA,GAAUC,sCAAAA,mBAAAA,CAAmB;IAAE;IAAS;IAAQ;GAAY,CAAC,CAAC;EAC/F;EACA,GAAG,YAAYC,0CAAAA,yBAAyB;GAAE,GAAG;GAAY;EAAS,CAAC;CACrE;;;;;;;;CASA,MAAM,UAAU,YAA2B;EACzC,IAAI;GACF,MAAM,MAAM;EACd,QAAQ,CAER;CACF;CAEA,OAAO;EACL;EACA,eAAe;GACb,WAAW;GACX,IAAI,oBAAoB,KAAA,GAAW,QAAQ,eAAe,eAAe;GACzE,kBAAkB,KAAA;EACpB;CACF;AACF"}
1
+ {"version":3,"file":"contextCatalog.cjs","names":["getHarnessState","resolvePluginEntries","readDoomMcpStatus","buildToolSources","extensionName","extensionPackageName","extensionToolSource","harnessRoot","buildSkillCatalog","readDoomSkillSourcesService","counter","projectContext","buildContextDetail","DOOM_CONTEXT_ENTRY_TYPE"],"sources":["../../../src/builders/cli/contextCatalog.ts"],"sourcesContent":["import { resolvePluginEntries } from '@agimon-ai/doompi-config/domains';\nimport { getHarnessState, harnessRoot } from '@agimon-ai/doompi-config/harnessStore';\nimport type { PackageAttribution } from '@agimon-ai/doompi-config/types';\nimport { buildContextDetail } from '@agimon-ai/doompi-core/context-detail';\nimport { DOOM_CONTEXT_ENTRY_TYPE, projectContext } from '@agimon-ai/doompi-core/context-projection';\nimport { readDoomMcpStatus } from '@agimon-ai/doompi-core/mcp-status';\nimport { readDoomSkillSourcesService } from '@agimon-ai/doompi-core/skills';\nimport type { ContextItemDetail, ContextPromptStage } from '@agimon-ai/doompi-core/types-context-api';\nimport { buildSkillCatalog, counter, type SkillEntry } from '@agimon-ai/doompi-skill/catalog';\nimport { extensionName, extensionPackageName, extensionToolSource } from '@agimon-ai/doompi-ui/extensionName';\nimport { buildToolSources } from '@agimon-ai/doompi-ui/toolInventory';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\n\n/**\n * Publishes what the session is composed of, and what it costs.\n *\n * Read at publish time rather than accumulated, because the answer changes for\n * reasons this module does not see: a reconnected MCP server, a domain switch,\n * a plan-mode tool swap. Reading the live inventory is cheap next to being\n * wrong about it.\n */\nexport interface ContextPublisher {\n /** Never rejects: callers fire this and forget it. */\n publish: () => Promise<void>;\n dispose: () => void;\n}\n\n/** The active minor modes at publish time, read rather than remembered. */\nexport type ReadMinorModes = () => readonly { readonly id: string; readonly label: string }[];\n\nexport interface ContextPublisherOptions {\n readMinorModes?: ReadMinorModes;\n /**\n * The session the detail file is keyed by, read at publish time.\n *\n * Undefined until a session is bound, and undefined in a host that has no\n * session at all; the projection is still journaled either way, and only the\n * click-through detail goes unwritten.\n */\n readSessionId?: () => string | undefined;\n /**\n * Where the click-through detail is left for the session API to find.\n *\n * Injected because writing it is filesystem work and this is not the layer\n * that does filesystem work. A host that supplies neither still gets the\n * projection; only the detail behind a row goes unpublished.\n */\n writeDetail?: (sessionId: string, revision: number, items: readonly ContextItemDetail[]) => void;\n removeDetail?: (sessionId: string) => void;\n /**\n * The system prompt as the session currently stands.\n *\n * Read rather than pushed, for the same reason the tool inventory is: the\n * prompt changes for reasons this module does not see. A host with no way to\n * answer returns undefined and the panel reports no prompt rather than a\n * stale one.\n */\n readSystemPrompt?: () => { readonly text: string; readonly stage: ContextPromptStage } | undefined;\n}\n\n/** Skills sit under owners, which is one level deeper than a flat list. */\nfunction flattenSkills(groups: Awaited<ReturnType<typeof buildSkillCatalog>>['groups']): SkillEntry[] {\n return groups.flatMap((group) => group.owners.flatMap((owner) => owner.skills));\n}\n\n/**\n * Package and plugin names to the mode that admitted them.\n *\n * Two halves meet here. Layer packages come from the harness, recorded when the\n * composition resolved. Domain plugins are re-resolved, because only the plugin\n * entry knows the domain that carried it.\n */\nfunction attributionFor(repoRoot: string, domains: readonly string[], pluginDirectories: readonly string[]) {\n const harness = getHarnessState();\n const attribution: Record<string, PackageAttribution> = { ...harness.packageAttribution };\n try {\n for (const entry of resolvePluginEntries(repoRoot, [...domains], [...pluginDirectories])) {\n if (entry.name && entry.domain) attribution[entry.name] = { kind: 'domain', mode: entry.domain };\n }\n } catch {\n // A domain that no longer resolves must not cost the reader the tool list.\n // Anything unmatched simply lands under core.\n }\n return attribution;\n}\n\nexport function createContextPublisher(\n pi: ExtensionAPI,\n cordis: Context,\n options: ContextPublisherOptions = {},\n): ContextPublisher {\n let published: string | undefined;\n let revision = 0;\n let disposed = false;\n /** Removed on disposal, so a session leaves nothing behind in the store. */\n let detailSessionId: string | undefined;\n\n const build = async (): Promise<void> => {\n if (disposed) return;\n const harness = getHarnessState();\n const mcpServers = readDoomMcpStatus(cordis)?.getSnapshot().servers;\n const sources = buildToolSources({\n tools: pi.getAllTools(),\n activeTools: pi.getActiveTools(),\n ...(mcpServers ? { mcpServers } : {}),\n resolveExtensionName: extensionName,\n resolveExtensionPackageName: extensionPackageName,\n resolveExtensionToolSource: (toolName) => extensionToolSource(pi, toolName),\n });\n\n const repoRoot = harnessRoot(harness);\n let skills: SkillEntry[] = [];\n try {\n const catalog = await buildSkillCatalog({\n repoRoot,\n activeSkillDirectories: harness.skillDirectories,\n extensionSources: readDoomSkillSourcesService(cordis)?.list() ?? [],\n });\n skills = flattenSkills(catalog.groups);\n } catch {\n // Tools are the larger cost and are already in hand; a skill walk that\n // fails should not take the whole figure down with it.\n }\n\n const countTokens = await counter();\n const prompt = options.readSystemPrompt?.();\n const promptCost = prompt === undefined ? undefined : { tokens: countTokens(prompt.text), stage: prompt.stage };\n const projection = projectContext({\n revision: revision + 1,\n majorMode: harness.majorMode,\n groups: (options.readMinorModes?.() ?? []).map((mode) => ({ ...mode, kind: 'minor' as const })),\n domains: harness.domains,\n sources,\n skills,\n attribution: attributionFor(repoRoot, harness.domains, harness.pluginDirectories),\n countTokens,\n ...(promptCost === undefined ? {} : { systemPrompt: promptCost }),\n });\n\n // Revision is compared out, so a republish that changed nothing is silent\n // rather than a new journal entry saying the same thing. The prompt joins by\n // its text: a reworded prompt of the same length is still a different answer.\n const serialized = JSON.stringify({ ...projection, revision: 0, promptText: prompt?.text });\n if (serialized === published || disposed) return;\n published = serialized;\n revision += 1;\n // The detail lands before the entry that invites a reader to ask for it, so\n // a click that follows the panel's update cannot outrun the file behind it.\n const sessionId = options.readSessionId?.();\n if (sessionId !== undefined && sessionId !== '' && options.writeDetail !== undefined) {\n detailSessionId = sessionId;\n options.writeDetail(\n sessionId,\n revision,\n buildContextDetail({\n sources,\n skills,\n countTokens,\n ...(prompt === undefined || promptCost === undefined\n ? {}\n : { systemPrompt: { text: prompt.text, tokens: promptCost.tokens, stage: prompt.stage } }),\n }),\n );\n }\n pi.appendEntry(DOOM_CONTEXT_ENTRY_TYPE, { ...projection, revision });\n };\n\n /**\n * Reporting the composition is a convenience, so it fails quietly.\n *\n * Callers publish without awaiting, and an escaping rejection would surface\n * as an unhandled error in a live session. A panel that cannot say what the\n * toolbox costs is a far smaller problem than that.\n */\n const publish = async (): Promise<void> => {\n try {\n await build();\n } catch {\n // Left unreported on purpose: the next composition change tries again.\n }\n };\n\n return {\n publish,\n dispose: () => {\n disposed = true;\n if (detailSessionId !== undefined) options.removeDetail?.(detailSessionId);\n detailSessionId = undefined;\n },\n };\n}\n"],"mappings":"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"}
@@ -57,6 +57,11 @@ function createContextPublisher(pi, cordis, options = {}) {
57
57
  })).groups);
58
58
  } catch {}
59
59
  const countTokens = await counter();
60
+ const prompt = options.readSystemPrompt?.();
61
+ const promptCost = prompt === void 0 ? void 0 : {
62
+ tokens: countTokens(prompt.text),
63
+ stage: prompt.stage
64
+ };
60
65
  const projection = projectContext({
61
66
  revision: revision + 1,
62
67
  majorMode: harness.majorMode,
@@ -68,11 +73,13 @@ function createContextPublisher(pi, cordis, options = {}) {
68
73
  sources,
69
74
  skills,
70
75
  attribution: attributionFor(repoRoot, harness.domains, harness.pluginDirectories),
71
- countTokens
76
+ countTokens,
77
+ ...promptCost === void 0 ? {} : { systemPrompt: promptCost }
72
78
  });
73
79
  const serialized = JSON.stringify({
74
80
  ...projection,
75
- revision: 0
81
+ revision: 0,
82
+ promptText: prompt?.text
76
83
  });
77
84
  if (serialized === published || disposed) return;
78
85
  published = serialized;
@@ -83,7 +90,12 @@ function createContextPublisher(pi, cordis, options = {}) {
83
90
  options.writeDetail(sessionId, revision, buildContextDetail({
84
91
  sources,
85
92
  skills,
86
- countTokens
93
+ countTokens,
94
+ ...prompt === void 0 || promptCost === void 0 ? {} : { systemPrompt: {
95
+ text: prompt.text,
96
+ tokens: promptCost.tokens,
97
+ stage: prompt.stage
98
+ } }
87
99
  }));
88
100
  }
89
101
  pi.appendEntry(DOOM_CONTEXT_ENTRY_TYPE, {
@@ -1 +1 @@
1
- {"version":3,"file":"contextCatalog.mjs","names":[],"sources":["../../../src/builders/cli/contextCatalog.ts"],"sourcesContent":["import { resolvePluginEntries } from '@agimon-ai/doompi-config/domains';\nimport { getHarnessState, harnessRoot } from '@agimon-ai/doompi-config/harnessStore';\nimport type { PackageAttribution } from '@agimon-ai/doompi-config/types';\nimport { buildContextDetail } from '@agimon-ai/doompi-core/context-detail';\nimport { DOOM_CONTEXT_ENTRY_TYPE, projectContext } from '@agimon-ai/doompi-core/context-projection';\nimport { readDoomMcpStatus } from '@agimon-ai/doompi-core/mcp-status';\nimport { readDoomSkillSourcesService } from '@agimon-ai/doompi-core/skills';\nimport type { ContextItemDetail } from '@agimon-ai/doompi-core/types-context-api';\nimport { buildSkillCatalog, counter, type SkillEntry } from '@agimon-ai/doompi-skill/catalog';\nimport { extensionName, extensionPackageName, extensionToolSource } from '@agimon-ai/doompi-ui/extensionName';\nimport { buildToolSources } from '@agimon-ai/doompi-ui/toolInventory';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\n\n/**\n * Publishes what the session is composed of, and what it costs.\n *\n * Read at publish time rather than accumulated, because the answer changes for\n * reasons this module does not see: a reconnected MCP server, a domain switch,\n * a plan-mode tool swap. Reading the live inventory is cheap next to being\n * wrong about it.\n */\nexport interface ContextPublisher {\n /** Never rejects: callers fire this and forget it. */\n publish: () => Promise<void>;\n dispose: () => void;\n}\n\n/** The active minor modes at publish time, read rather than remembered. */\nexport type ReadMinorModes = () => readonly { readonly id: string; readonly label: string }[];\n\nexport interface ContextPublisherOptions {\n readMinorModes?: ReadMinorModes;\n /**\n * The session the detail file is keyed by, read at publish time.\n *\n * Undefined until a session is bound, and undefined in a host that has no\n * session at all; the projection is still journaled either way, and only the\n * click-through detail goes unwritten.\n */\n readSessionId?: () => string | undefined;\n /**\n * Where the click-through detail is left for the session API to find.\n *\n * Injected because writing it is filesystem work and this is not the layer\n * that does filesystem work. A host that supplies neither still gets the\n * projection; only the detail behind a row goes unpublished.\n */\n writeDetail?: (sessionId: string, revision: number, items: readonly ContextItemDetail[]) => void;\n removeDetail?: (sessionId: string) => void;\n}\n\n/** Skills sit under owners, which is one level deeper than a flat list. */\nfunction flattenSkills(groups: Awaited<ReturnType<typeof buildSkillCatalog>>['groups']): SkillEntry[] {\n return groups.flatMap((group) => group.owners.flatMap((owner) => owner.skills));\n}\n\n/**\n * Package and plugin names to the mode that admitted them.\n *\n * Two halves meet here. Layer packages come from the harness, recorded when the\n * composition resolved. Domain plugins are re-resolved, because only the plugin\n * entry knows the domain that carried it.\n */\nfunction attributionFor(repoRoot: string, domains: readonly string[], pluginDirectories: readonly string[]) {\n const harness = getHarnessState();\n const attribution: Record<string, PackageAttribution> = { ...harness.packageAttribution };\n try {\n for (const entry of resolvePluginEntries(repoRoot, [...domains], [...pluginDirectories])) {\n if (entry.name && entry.domain) attribution[entry.name] = { kind: 'domain', mode: entry.domain };\n }\n } catch {\n // A domain that no longer resolves must not cost the reader the tool list.\n // Anything unmatched simply lands under core.\n }\n return attribution;\n}\n\nexport function createContextPublisher(\n pi: ExtensionAPI,\n cordis: Context,\n options: ContextPublisherOptions = {},\n): ContextPublisher {\n let published: string | undefined;\n let revision = 0;\n let disposed = false;\n /** Removed on disposal, so a session leaves nothing behind in the store. */\n let detailSessionId: string | undefined;\n\n const build = async (): Promise<void> => {\n if (disposed) return;\n const harness = getHarnessState();\n const mcpServers = readDoomMcpStatus(cordis)?.getSnapshot().servers;\n const sources = buildToolSources({\n tools: pi.getAllTools(),\n activeTools: pi.getActiveTools(),\n ...(mcpServers ? { mcpServers } : {}),\n resolveExtensionName: extensionName,\n resolveExtensionPackageName: extensionPackageName,\n resolveExtensionToolSource: (toolName) => extensionToolSource(pi, toolName),\n });\n\n const repoRoot = harnessRoot(harness);\n let skills: SkillEntry[] = [];\n try {\n const catalog = await buildSkillCatalog({\n repoRoot,\n activeSkillDirectories: harness.skillDirectories,\n extensionSources: readDoomSkillSourcesService(cordis)?.list() ?? [],\n });\n skills = flattenSkills(catalog.groups);\n } catch {\n // Tools are the larger cost and are already in hand; a skill walk that\n // fails should not take the whole figure down with it.\n }\n\n const countTokens = await counter();\n const projection = projectContext({\n revision: revision + 1,\n majorMode: harness.majorMode,\n groups: (options.readMinorModes?.() ?? []).map((mode) => ({ ...mode, kind: 'minor' as const })),\n domains: harness.domains,\n sources,\n skills,\n attribution: attributionFor(repoRoot, harness.domains, harness.pluginDirectories),\n countTokens,\n });\n\n // Revision is compared out, so a republish that changed nothing is silent\n // rather than a new journal entry saying the same thing.\n const serialized = JSON.stringify({ ...projection, revision: 0 });\n if (serialized === published || disposed) return;\n published = serialized;\n revision += 1;\n // The detail lands before the entry that invites a reader to ask for it, so\n // a click that follows the panel's update cannot outrun the file behind it.\n const sessionId = options.readSessionId?.();\n if (sessionId !== undefined && sessionId !== '' && options.writeDetail !== undefined) {\n detailSessionId = sessionId;\n options.writeDetail(sessionId, revision, buildContextDetail({ sources, skills, countTokens }));\n }\n pi.appendEntry(DOOM_CONTEXT_ENTRY_TYPE, { ...projection, revision });\n };\n\n /**\n * Reporting the composition is a convenience, so it fails quietly.\n *\n * Callers publish without awaiting, and an escaping rejection would surface\n * as an unhandled error in a live session. A panel that cannot say what the\n * toolbox costs is a far smaller problem than that.\n */\n const publish = async (): Promise<void> => {\n try {\n await build();\n } catch {\n // Left unreported on purpose: the next composition change tries again.\n }\n };\n\n return {\n publish,\n dispose: () => {\n disposed = true;\n if (detailSessionId !== undefined) options.removeDetail?.(detailSessionId);\n detailSessionId = undefined;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;AAqDA,SAAS,cAAc,QAA+E;CACpG,OAAO,OAAO,SAAS,UAAU,MAAM,OAAO,SAAS,UAAU,MAAM,MAAM,CAAC;AAChF;;;;;;;;AASA,SAAS,eAAe,UAAkB,SAA4B,mBAAsC;CAE1G,MAAM,cAAkD,EAAE,GAD1C,gBACmD,CAAC,CAAC,mBAAmB;CACxF,IAAI;EACF,KAAK,MAAM,SAAS,qBAAqB,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,GAAG,iBAAiB,CAAC,GACrF,IAAI,MAAM,QAAQ,MAAM,QAAQ,YAAY,MAAM,QAAQ;GAAE,MAAM;GAAU,MAAM,MAAM;EAAO;CAEnG,QAAQ,CAGR;CACA,OAAO;AACT;AAEA,SAAgB,uBACd,IACA,QACA,UAAmC,CAAC,GAClB;CAClB,IAAI;CACJ,IAAI,WAAW;CACf,IAAI,WAAW;;CAEf,IAAI;CAEJ,MAAM,QAAQ,YAA2B;EACvC,IAAI,UAAU;EACd,MAAM,UAAU,gBAAgB;EAChC,MAAM,aAAa,kBAAkB,MAAM,CAAC,EAAE,YAAY,CAAC,CAAC;EAC5D,MAAM,UAAU,iBAAiB;GAC/B,OAAO,GAAG,YAAY;GACtB,aAAa,GAAG,eAAe;GAC/B,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;GACnC,sBAAsB;GACtB,6BAA6B;GAC7B,6BAA6B,aAAa,oBAAoB,IAAI,QAAQ;EAC5E,CAAC;EAED,MAAM,WAAW,YAAY,OAAO;EACpC,IAAI,SAAuB,CAAC;EAC5B,IAAI;GAMF,SAAS,eAAc,MALD,kBAAkB;IACtC;IACA,wBAAwB,QAAQ;IAChC,kBAAkB,4BAA4B,MAAM,CAAC,EAAE,KAAK,KAAK,CAAC;GACpE,CAAC,EAAA,CAC8B,MAAM;EACvC,QAAQ,CAGR;EAEA,MAAM,cAAc,MAAM,QAAQ;EAClC,MAAM,aAAa,eAAe;GAChC,UAAU,WAAW;GACrB,WAAW,QAAQ;GACnB,SAAS,QAAQ,iBAAiB,KAAK,CAAC,EAAA,CAAG,KAAK,UAAU;IAAE,GAAG;IAAM,MAAM;GAAiB,EAAE;GAC9F,SAAS,QAAQ;GACjB;GACA;GACA,aAAa,eAAe,UAAU,QAAQ,SAAS,QAAQ,iBAAiB;GAChF;EACF,CAAC;EAID,MAAM,aAAa,KAAK,UAAU;GAAE,GAAG;GAAY,UAAU;EAAE,CAAC;EAChE,IAAI,eAAe,aAAa,UAAU;EAC1C,YAAY;EACZ,YAAY;EAGZ,MAAM,YAAY,QAAQ,gBAAgB;EAC1C,IAAI,cAAc,KAAA,KAAa,cAAc,MAAM,QAAQ,gBAAgB,KAAA,GAAW;GACpF,kBAAkB;GAClB,QAAQ,YAAY,WAAW,UAAU,mBAAmB;IAAE;IAAS;IAAQ;GAAY,CAAC,CAAC;EAC/F;EACA,GAAG,YAAY,yBAAyB;GAAE,GAAG;GAAY;EAAS,CAAC;CACrE;;;;;;;;CASA,MAAM,UAAU,YAA2B;EACzC,IAAI;GACF,MAAM,MAAM;EACd,QAAQ,CAER;CACF;CAEA,OAAO;EACL;EACA,eAAe;GACb,WAAW;GACX,IAAI,oBAAoB,KAAA,GAAW,QAAQ,eAAe,eAAe;GACzE,kBAAkB,KAAA;EACpB;CACF;AACF"}
1
+ {"version":3,"file":"contextCatalog.mjs","names":[],"sources":["../../../src/builders/cli/contextCatalog.ts"],"sourcesContent":["import { resolvePluginEntries } from '@agimon-ai/doompi-config/domains';\nimport { getHarnessState, harnessRoot } from '@agimon-ai/doompi-config/harnessStore';\nimport type { PackageAttribution } from '@agimon-ai/doompi-config/types';\nimport { buildContextDetail } from '@agimon-ai/doompi-core/context-detail';\nimport { DOOM_CONTEXT_ENTRY_TYPE, projectContext } from '@agimon-ai/doompi-core/context-projection';\nimport { readDoomMcpStatus } from '@agimon-ai/doompi-core/mcp-status';\nimport { readDoomSkillSourcesService } from '@agimon-ai/doompi-core/skills';\nimport type { ContextItemDetail, ContextPromptStage } from '@agimon-ai/doompi-core/types-context-api';\nimport { buildSkillCatalog, counter, type SkillEntry } from '@agimon-ai/doompi-skill/catalog';\nimport { extensionName, extensionPackageName, extensionToolSource } from '@agimon-ai/doompi-ui/extensionName';\nimport { buildToolSources } from '@agimon-ai/doompi-ui/toolInventory';\nimport type { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\n\n/**\n * Publishes what the session is composed of, and what it costs.\n *\n * Read at publish time rather than accumulated, because the answer changes for\n * reasons this module does not see: a reconnected MCP server, a domain switch,\n * a plan-mode tool swap. Reading the live inventory is cheap next to being\n * wrong about it.\n */\nexport interface ContextPublisher {\n /** Never rejects: callers fire this and forget it. */\n publish: () => Promise<void>;\n dispose: () => void;\n}\n\n/** The active minor modes at publish time, read rather than remembered. */\nexport type ReadMinorModes = () => readonly { readonly id: string; readonly label: string }[];\n\nexport interface ContextPublisherOptions {\n readMinorModes?: ReadMinorModes;\n /**\n * The session the detail file is keyed by, read at publish time.\n *\n * Undefined until a session is bound, and undefined in a host that has no\n * session at all; the projection is still journaled either way, and only the\n * click-through detail goes unwritten.\n */\n readSessionId?: () => string | undefined;\n /**\n * Where the click-through detail is left for the session API to find.\n *\n * Injected because writing it is filesystem work and this is not the layer\n * that does filesystem work. A host that supplies neither still gets the\n * projection; only the detail behind a row goes unpublished.\n */\n writeDetail?: (sessionId: string, revision: number, items: readonly ContextItemDetail[]) => void;\n removeDetail?: (sessionId: string) => void;\n /**\n * The system prompt as the session currently stands.\n *\n * Read rather than pushed, for the same reason the tool inventory is: the\n * prompt changes for reasons this module does not see. A host with no way to\n * answer returns undefined and the panel reports no prompt rather than a\n * stale one.\n */\n readSystemPrompt?: () => { readonly text: string; readonly stage: ContextPromptStage } | undefined;\n}\n\n/** Skills sit under owners, which is one level deeper than a flat list. */\nfunction flattenSkills(groups: Awaited<ReturnType<typeof buildSkillCatalog>>['groups']): SkillEntry[] {\n return groups.flatMap((group) => group.owners.flatMap((owner) => owner.skills));\n}\n\n/**\n * Package and plugin names to the mode that admitted them.\n *\n * Two halves meet here. Layer packages come from the harness, recorded when the\n * composition resolved. Domain plugins are re-resolved, because only the plugin\n * entry knows the domain that carried it.\n */\nfunction attributionFor(repoRoot: string, domains: readonly string[], pluginDirectories: readonly string[]) {\n const harness = getHarnessState();\n const attribution: Record<string, PackageAttribution> = { ...harness.packageAttribution };\n try {\n for (const entry of resolvePluginEntries(repoRoot, [...domains], [...pluginDirectories])) {\n if (entry.name && entry.domain) attribution[entry.name] = { kind: 'domain', mode: entry.domain };\n }\n } catch {\n // A domain that no longer resolves must not cost the reader the tool list.\n // Anything unmatched simply lands under core.\n }\n return attribution;\n}\n\nexport function createContextPublisher(\n pi: ExtensionAPI,\n cordis: Context,\n options: ContextPublisherOptions = {},\n): ContextPublisher {\n let published: string | undefined;\n let revision = 0;\n let disposed = false;\n /** Removed on disposal, so a session leaves nothing behind in the store. */\n let detailSessionId: string | undefined;\n\n const build = async (): Promise<void> => {\n if (disposed) return;\n const harness = getHarnessState();\n const mcpServers = readDoomMcpStatus(cordis)?.getSnapshot().servers;\n const sources = buildToolSources({\n tools: pi.getAllTools(),\n activeTools: pi.getActiveTools(),\n ...(mcpServers ? { mcpServers } : {}),\n resolveExtensionName: extensionName,\n resolveExtensionPackageName: extensionPackageName,\n resolveExtensionToolSource: (toolName) => extensionToolSource(pi, toolName),\n });\n\n const repoRoot = harnessRoot(harness);\n let skills: SkillEntry[] = [];\n try {\n const catalog = await buildSkillCatalog({\n repoRoot,\n activeSkillDirectories: harness.skillDirectories,\n extensionSources: readDoomSkillSourcesService(cordis)?.list() ?? [],\n });\n skills = flattenSkills(catalog.groups);\n } catch {\n // Tools are the larger cost and are already in hand; a skill walk that\n // fails should not take the whole figure down with it.\n }\n\n const countTokens = await counter();\n const prompt = options.readSystemPrompt?.();\n const promptCost = prompt === undefined ? undefined : { tokens: countTokens(prompt.text), stage: prompt.stage };\n const projection = projectContext({\n revision: revision + 1,\n majorMode: harness.majorMode,\n groups: (options.readMinorModes?.() ?? []).map((mode) => ({ ...mode, kind: 'minor' as const })),\n domains: harness.domains,\n sources,\n skills,\n attribution: attributionFor(repoRoot, harness.domains, harness.pluginDirectories),\n countTokens,\n ...(promptCost === undefined ? {} : { systemPrompt: promptCost }),\n });\n\n // Revision is compared out, so a republish that changed nothing is silent\n // rather than a new journal entry saying the same thing. The prompt joins by\n // its text: a reworded prompt of the same length is still a different answer.\n const serialized = JSON.stringify({ ...projection, revision: 0, promptText: prompt?.text });\n if (serialized === published || disposed) return;\n published = serialized;\n revision += 1;\n // The detail lands before the entry that invites a reader to ask for it, so\n // a click that follows the panel's update cannot outrun the file behind it.\n const sessionId = options.readSessionId?.();\n if (sessionId !== undefined && sessionId !== '' && options.writeDetail !== undefined) {\n detailSessionId = sessionId;\n options.writeDetail(\n sessionId,\n revision,\n buildContextDetail({\n sources,\n skills,\n countTokens,\n ...(prompt === undefined || promptCost === undefined\n ? {}\n : { systemPrompt: { text: prompt.text, tokens: promptCost.tokens, stage: prompt.stage } }),\n }),\n );\n }\n pi.appendEntry(DOOM_CONTEXT_ENTRY_TYPE, { ...projection, revision });\n };\n\n /**\n * Reporting the composition is a convenience, so it fails quietly.\n *\n * Callers publish without awaiting, and an escaping rejection would surface\n * as an unhandled error in a live session. A panel that cannot say what the\n * toolbox costs is a far smaller problem than that.\n */\n const publish = async (): Promise<void> => {\n try {\n await build();\n } catch {\n // Left unreported on purpose: the next composition change tries again.\n }\n };\n\n return {\n publish,\n dispose: () => {\n disposed = true;\n if (detailSessionId !== undefined) options.removeDetail?.(detailSessionId);\n detailSessionId = undefined;\n },\n };\n}\n"],"mappings":"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"}
@@ -57,7 +57,7 @@ function resolveSettingsPath(value, baseDirectory, homeDirectory) {
57
57
  *
58
58
  * The bare package name is matched textually because that is the spelling sync
59
59
  * writes into the user scope, and a resolved path is matched by walking to the
60
- * package it belongs to, which is what catches `../packages/core/doompi` and a
60
+ * package it belongs to, which is what catches `../packages/cli/doompi` and a
61
61
  * direct reference to a file inside it. Pattern entries select among already
62
62
  * resolved paths rather than naming one, so they are left for the user to own.
63
63
  */
@@ -1 +1 @@
1
- {"version":3,"file":"projectSettings.cjs","names":["path","DOOM_PACKAGE_NAME","isDoomPackagePath","isRecord","os","fs","readJson","piExtensionAliasPath"],"sources":["../../../src/builders/cli/projectSettings.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { DOOM_PACKAGE_NAME, isDoomPackagePath } from '@agimon-ai/doompi-core/doom-package';\nimport { isRecord, readJson, writeFileAtomic } from '@agimon-ai/doompi-core/runtime-json';\nimport type { JsonObject } from '@agimon-ai/doompi-core/runtime-json';\n\nimport { piExtensionAliasPath } from './piExtensionAlias';\n\n/**\n * Pi's project settings, as `doompi sync` reconciles them.\n *\n * Pi documents project settings as overriding user settings, but resource\n * resolution unions the two scopes and dedupes only by the resolved file's real\n * path. A repository that registers DoomPi alongside the stable user entry\n * therefore loads two installs of the same package against one home-scoped\n * worktree state. Sync removes the redundant project registration and leaves every\n * other key alone: the repository still overrides the user scope, through its own\n * `.doom` configuration and the runtime staged under `~/.pi/.doom/sync`.\n */\n\nconst PI_DIRECTORY = '.pi';\nconst SETTINGS_FILE = 'settings.json';\nconst EXTENSIONS_KEY = 'extensions';\nconst PACKAGES_KEY = 'packages';\nconst PACKAGE_SOURCE_KEY = 'source';\nconst HOME_ALIAS = '~';\nconst HOME_ALIAS_PREFIX = '~/';\n/** Pattern-form entries Pi filters with rather than resolves. */\nconst PATTERN_PREFIXES = ['!', '+', '-'] as const;\n\n/**\n * Reported by `sync --check` and `build`, and repaired by `sync`.\n *\n * Lives here rather than with either command because both report the same\n * finding about the same file, and the detector below is what decides it.\n */\nexport const DUPLICATE_REGISTRATION_DRIFT = 'duplicate DoomPi registration in .pi/settings.json';\n\n/** Pi's project configuration directory, the base its relative paths resolve against. */\nexport function projectPiDirectory(repoRoot: string): string {\n return path.join(repoRoot, PI_DIRECTORY);\n}\n\nexport function projectPiSettingsPath(repoRoot: string): string {\n return path.join(projectPiDirectory(repoRoot), SETTINGS_FILE);\n}\n\nfunction resolveSettingsPath(value: string, baseDirectory: string, homeDirectory: string): string {\n if (value === HOME_ALIAS) return homeDirectory;\n if (value.startsWith(HOME_ALIAS_PREFIX)) return path.join(homeDirectory, value.slice(HOME_ALIAS_PREFIX.length));\n return path.resolve(baseDirectory, value);\n}\n\n/**\n * Whether one settings entry names this package.\n *\n * The bare package name is matched textually because that is the spelling sync\n * writes into the user scope, and a resolved path is matched by walking to the\n * package it belongs to, which is what catches `../packages/core/doompi` and a\n * direct reference to a file inside it. Pattern entries select among already\n * resolved paths rather than naming one, so they are left for the user to own.\n */\nfunction isDoomEntry(value: string, baseDirectory: string, homeDirectory: string): boolean {\n const normalized = value.replaceAll('\\\\', '/').trim();\n if (normalized === DOOM_PACKAGE_NAME) return true;\n if (PATTERN_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;\n return isDoomPackagePath(resolveSettingsPath(normalized, baseDirectory, homeDirectory));\n}\n\nfunction packageSource(entry: unknown): string | undefined {\n if (typeof entry === 'string') return entry;\n if (isRecord(entry) && typeof entry[PACKAGE_SOURCE_KEY] === 'string') return entry[PACKAGE_SOURCE_KEY];\n return undefined;\n}\n\n/**\n * Drops DoomPi from one resource list, or reports that the key should go.\n *\n * A list that empties out is removed rather than left behind: an empty array is\n * indistinguishable from an absent one to Pi, and keeping it would leave the\n * repository looking like it still configures something.\n */\nfunction withoutDoomEntries(value: unknown, baseDirectory: string, homeDirectory: string): unknown {\n if (!Array.isArray(value)) return value;\n const kept = value.filter((entry) => {\n const source = packageSource(entry);\n return source === undefined || !isDoomEntry(source, baseDirectory, homeDirectory);\n });\n if (kept.length === value.length) return value;\n return kept.length > 0 ? kept : undefined;\n}\n\n/** Produces the project settings content for a reconciliation, without writing it. */\nexport function mergeProjectPiSettings(\n current: JsonObject,\n repoRoot: string,\n homeDirectory: string = os.homedir(),\n): JsonObject {\n const baseDirectory = projectPiDirectory(repoRoot);\n const merged: JsonObject = { ...current };\n for (const key of [EXTENSIONS_KEY, PACKAGES_KEY]) {\n if (!(key in merged)) continue;\n const next = withoutDoomEntries(merged[key], baseDirectory, homeDirectory);\n if (next === undefined) delete merged[key];\n else merged[key] = next;\n }\n return merged;\n}\n\n/** Reads Pi's project settings, or nothing when the repository declares none. */\nexport function readProjectPiSettings(repoRoot: string): JsonObject | undefined {\n const settingsPath = projectPiSettingsPath(repoRoot);\n if (!fs.existsSync(settingsPath)) return undefined;\n const settings = readJson(settingsPath);\n return isRecord(settings) ? settings : {};\n}\n\nexport function serializeProjectPiSettings(settings: JsonObject): string {\n return `${JSON.stringify(settings, null, 2)}\\n`;\n}\n\n/** True when the repository still registers DoomPi alongside the user scope. */\nexport function projectRegistersDoom(repoRoot: string, homeDirectory: string = os.homedir()): boolean {\n const settings = readProjectPiSettings(repoRoot);\n if (!settings) return false;\n return (\n serializeProjectPiSettings(mergeProjectPiSettings(settings, repoRoot, homeDirectory)) !==\n serializeProjectPiSettings(settings)\n );\n}\n\n/**\n * Removes the alias a repository-scoped registration needed.\n *\n * Only a symbolic link is removed, for the same reason sync refuses to replace\n * one: anything else at that path was not created here.\n */\nfunction removeProjectExtensionAlias(repoRoot: string): void {\n const aliasPath = piExtensionAliasPath(projectPiDirectory(repoRoot));\n if (fs.lstatSync(aliasPath, { throwIfNoEntry: false })?.isSymbolicLink()) {\n fs.rmSync(aliasPath, { force: true });\n }\n}\n\n/**\n * Reconciles the repository's Pi settings with the user-scope registration.\n *\n * The file is never created and never deleted, only rewritten: an absent one\n * means the repository registers nothing, and an existing one is a repository\n * root marker that other DoomPi code walks up to find.\n */\nexport function writeProjectPiSettings(repoRoot: string, homeDirectory: string = os.homedir()): string | undefined {\n const settings = readProjectPiSettings(repoRoot);\n removeProjectExtensionAlias(repoRoot);\n if (!settings) return undefined;\n\n const merged = mergeProjectPiSettings(settings, repoRoot, homeDirectory);\n const serialized = serializeProjectPiSettings(merged);\n if (serialized === serializeProjectPiSettings(settings)) return undefined;\n\n const settingsPath = projectPiSettingsPath(repoRoot);\n writeFileAtomic(settingsPath, serialized);\n return settingsPath;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projectSettings.cjs","names":["path","DOOM_PACKAGE_NAME","isDoomPackagePath","isRecord","os","fs","readJson","piExtensionAliasPath"],"sources":["../../../src/builders/cli/projectSettings.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { DOOM_PACKAGE_NAME, isDoomPackagePath } from '@agimon-ai/doompi-core/doom-package';\nimport { isRecord, readJson, writeFileAtomic } from '@agimon-ai/doompi-core/runtime-json';\nimport type { JsonObject } from '@agimon-ai/doompi-core/runtime-json';\n\nimport { piExtensionAliasPath } from './piExtensionAlias';\n\n/**\n * Pi's project settings, as `doompi sync` reconciles them.\n *\n * Pi documents project settings as overriding user settings, but resource\n * resolution unions the two scopes and dedupes only by the resolved file's real\n * path. A repository that registers DoomPi alongside the stable user entry\n * therefore loads two installs of the same package against one home-scoped\n * worktree state. Sync removes the redundant project registration and leaves every\n * other key alone: the repository still overrides the user scope, through its own\n * `.doom` configuration and the runtime staged under `~/.pi/.doom/sync`.\n */\n\nconst PI_DIRECTORY = '.pi';\nconst SETTINGS_FILE = 'settings.json';\nconst EXTENSIONS_KEY = 'extensions';\nconst PACKAGES_KEY = 'packages';\nconst PACKAGE_SOURCE_KEY = 'source';\nconst HOME_ALIAS = '~';\nconst HOME_ALIAS_PREFIX = '~/';\n/** Pattern-form entries Pi filters with rather than resolves. */\nconst PATTERN_PREFIXES = ['!', '+', '-'] as const;\n\n/**\n * Reported by `sync --check` and `build`, and repaired by `sync`.\n *\n * Lives here rather than with either command because both report the same\n * finding about the same file, and the detector below is what decides it.\n */\nexport const DUPLICATE_REGISTRATION_DRIFT = 'duplicate DoomPi registration in .pi/settings.json';\n\n/** Pi's project configuration directory, the base its relative paths resolve against. */\nexport function projectPiDirectory(repoRoot: string): string {\n return path.join(repoRoot, PI_DIRECTORY);\n}\n\nexport function projectPiSettingsPath(repoRoot: string): string {\n return path.join(projectPiDirectory(repoRoot), SETTINGS_FILE);\n}\n\nfunction resolveSettingsPath(value: string, baseDirectory: string, homeDirectory: string): string {\n if (value === HOME_ALIAS) return homeDirectory;\n if (value.startsWith(HOME_ALIAS_PREFIX)) return path.join(homeDirectory, value.slice(HOME_ALIAS_PREFIX.length));\n return path.resolve(baseDirectory, value);\n}\n\n/**\n * Whether one settings entry names this package.\n *\n * The bare package name is matched textually because that is the spelling sync\n * writes into the user scope, and a resolved path is matched by walking to the\n * package it belongs to, which is what catches `../packages/cli/doompi` and a\n * direct reference to a file inside it. Pattern entries select among already\n * resolved paths rather than naming one, so they are left for the user to own.\n */\nfunction isDoomEntry(value: string, baseDirectory: string, homeDirectory: string): boolean {\n const normalized = value.replaceAll('\\\\', '/').trim();\n if (normalized === DOOM_PACKAGE_NAME) return true;\n if (PATTERN_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;\n return isDoomPackagePath(resolveSettingsPath(normalized, baseDirectory, homeDirectory));\n}\n\nfunction packageSource(entry: unknown): string | undefined {\n if (typeof entry === 'string') return entry;\n if (isRecord(entry) && typeof entry[PACKAGE_SOURCE_KEY] === 'string') return entry[PACKAGE_SOURCE_KEY];\n return undefined;\n}\n\n/**\n * Drops DoomPi from one resource list, or reports that the key should go.\n *\n * A list that empties out is removed rather than left behind: an empty array is\n * indistinguishable from an absent one to Pi, and keeping it would leave the\n * repository looking like it still configures something.\n */\nfunction withoutDoomEntries(value: unknown, baseDirectory: string, homeDirectory: string): unknown {\n if (!Array.isArray(value)) return value;\n const kept = value.filter((entry) => {\n const source = packageSource(entry);\n return source === undefined || !isDoomEntry(source, baseDirectory, homeDirectory);\n });\n if (kept.length === value.length) return value;\n return kept.length > 0 ? kept : undefined;\n}\n\n/** Produces the project settings content for a reconciliation, without writing it. */\nexport function mergeProjectPiSettings(\n current: JsonObject,\n repoRoot: string,\n homeDirectory: string = os.homedir(),\n): JsonObject {\n const baseDirectory = projectPiDirectory(repoRoot);\n const merged: JsonObject = { ...current };\n for (const key of [EXTENSIONS_KEY, PACKAGES_KEY]) {\n if (!(key in merged)) continue;\n const next = withoutDoomEntries(merged[key], baseDirectory, homeDirectory);\n if (next === undefined) delete merged[key];\n else merged[key] = next;\n }\n return merged;\n}\n\n/** Reads Pi's project settings, or nothing when the repository declares none. */\nexport function readProjectPiSettings(repoRoot: string): JsonObject | undefined {\n const settingsPath = projectPiSettingsPath(repoRoot);\n if (!fs.existsSync(settingsPath)) return undefined;\n const settings = readJson(settingsPath);\n return isRecord(settings) ? settings : {};\n}\n\nexport function serializeProjectPiSettings(settings: JsonObject): string {\n return `${JSON.stringify(settings, null, 2)}\\n`;\n}\n\n/** True when the repository still registers DoomPi alongside the user scope. */\nexport function projectRegistersDoom(repoRoot: string, homeDirectory: string = os.homedir()): boolean {\n const settings = readProjectPiSettings(repoRoot);\n if (!settings) return false;\n return (\n serializeProjectPiSettings(mergeProjectPiSettings(settings, repoRoot, homeDirectory)) !==\n serializeProjectPiSettings(settings)\n );\n}\n\n/**\n * Removes the alias a repository-scoped registration needed.\n *\n * Only a symbolic link is removed, for the same reason sync refuses to replace\n * one: anything else at that path was not created here.\n */\nfunction removeProjectExtensionAlias(repoRoot: string): void {\n const aliasPath = piExtensionAliasPath(projectPiDirectory(repoRoot));\n if (fs.lstatSync(aliasPath, { throwIfNoEntry: false })?.isSymbolicLink()) {\n fs.rmSync(aliasPath, { force: true });\n }\n}\n\n/**\n * Reconciles the repository's Pi settings with the user-scope registration.\n *\n * The file is never created and never deleted, only rewritten: an absent one\n * means the repository registers nothing, and an existing one is a repository\n * root marker that other DoomPi code walks up to find.\n */\nexport function writeProjectPiSettings(repoRoot: string, homeDirectory: string = os.homedir()): string | undefined {\n const settings = readProjectPiSettings(repoRoot);\n removeProjectExtensionAlias(repoRoot);\n if (!settings) return undefined;\n\n const merged = mergeProjectPiSettings(settings, repoRoot, homeDirectory);\n const serialized = serializeProjectPiSettings(merged);\n if (serialized === serializeProjectPiSettings(settings)) return undefined;\n\n const settingsPath = projectPiSettingsPath(repoRoot);\n writeFileAtomic(settingsPath, serialized);\n return settingsPath;\n}\n"],"mappings":"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"}
@@ -53,7 +53,7 @@ function resolveSettingsPath(value, baseDirectory, homeDirectory) {
53
53
  *
54
54
  * The bare package name is matched textually because that is the spelling sync
55
55
  * writes into the user scope, and a resolved path is matched by walking to the
56
- * package it belongs to, which is what catches `../packages/core/doompi` and a
56
+ * package it belongs to, which is what catches `../packages/cli/doompi` and a
57
57
  * direct reference to a file inside it. Pattern entries select among already
58
58
  * resolved paths rather than naming one, so they are left for the user to own.
59
59
  */
@@ -1 +1 @@
1
- {"version":3,"file":"projectSettings.mjs","names":[],"sources":["../../../src/builders/cli/projectSettings.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { DOOM_PACKAGE_NAME, isDoomPackagePath } from '@agimon-ai/doompi-core/doom-package';\nimport { isRecord, readJson, writeFileAtomic } from '@agimon-ai/doompi-core/runtime-json';\nimport type { JsonObject } from '@agimon-ai/doompi-core/runtime-json';\n\nimport { piExtensionAliasPath } from './piExtensionAlias';\n\n/**\n * Pi's project settings, as `doompi sync` reconciles them.\n *\n * Pi documents project settings as overriding user settings, but resource\n * resolution unions the two scopes and dedupes only by the resolved file's real\n * path. A repository that registers DoomPi alongside the stable user entry\n * therefore loads two installs of the same package against one home-scoped\n * worktree state. Sync removes the redundant project registration and leaves every\n * other key alone: the repository still overrides the user scope, through its own\n * `.doom` configuration and the runtime staged under `~/.pi/.doom/sync`.\n */\n\nconst PI_DIRECTORY = '.pi';\nconst SETTINGS_FILE = 'settings.json';\nconst EXTENSIONS_KEY = 'extensions';\nconst PACKAGES_KEY = 'packages';\nconst PACKAGE_SOURCE_KEY = 'source';\nconst HOME_ALIAS = '~';\nconst HOME_ALIAS_PREFIX = '~/';\n/** Pattern-form entries Pi filters with rather than resolves. */\nconst PATTERN_PREFIXES = ['!', '+', '-'] as const;\n\n/**\n * Reported by `sync --check` and `build`, and repaired by `sync`.\n *\n * Lives here rather than with either command because both report the same\n * finding about the same file, and the detector below is what decides it.\n */\nexport const DUPLICATE_REGISTRATION_DRIFT = 'duplicate DoomPi registration in .pi/settings.json';\n\n/** Pi's project configuration directory, the base its relative paths resolve against. */\nexport function projectPiDirectory(repoRoot: string): string {\n return path.join(repoRoot, PI_DIRECTORY);\n}\n\nexport function projectPiSettingsPath(repoRoot: string): string {\n return path.join(projectPiDirectory(repoRoot), SETTINGS_FILE);\n}\n\nfunction resolveSettingsPath(value: string, baseDirectory: string, homeDirectory: string): string {\n if (value === HOME_ALIAS) return homeDirectory;\n if (value.startsWith(HOME_ALIAS_PREFIX)) return path.join(homeDirectory, value.slice(HOME_ALIAS_PREFIX.length));\n return path.resolve(baseDirectory, value);\n}\n\n/**\n * Whether one settings entry names this package.\n *\n * The bare package name is matched textually because that is the spelling sync\n * writes into the user scope, and a resolved path is matched by walking to the\n * package it belongs to, which is what catches `../packages/core/doompi` and a\n * direct reference to a file inside it. Pattern entries select among already\n * resolved paths rather than naming one, so they are left for the user to own.\n */\nfunction isDoomEntry(value: string, baseDirectory: string, homeDirectory: string): boolean {\n const normalized = value.replaceAll('\\\\', '/').trim();\n if (normalized === DOOM_PACKAGE_NAME) return true;\n if (PATTERN_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;\n return isDoomPackagePath(resolveSettingsPath(normalized, baseDirectory, homeDirectory));\n}\n\nfunction packageSource(entry: unknown): string | undefined {\n if (typeof entry === 'string') return entry;\n if (isRecord(entry) && typeof entry[PACKAGE_SOURCE_KEY] === 'string') return entry[PACKAGE_SOURCE_KEY];\n return undefined;\n}\n\n/**\n * Drops DoomPi from one resource list, or reports that the key should go.\n *\n * A list that empties out is removed rather than left behind: an empty array is\n * indistinguishable from an absent one to Pi, and keeping it would leave the\n * repository looking like it still configures something.\n */\nfunction withoutDoomEntries(value: unknown, baseDirectory: string, homeDirectory: string): unknown {\n if (!Array.isArray(value)) return value;\n const kept = value.filter((entry) => {\n const source = packageSource(entry);\n return source === undefined || !isDoomEntry(source, baseDirectory, homeDirectory);\n });\n if (kept.length === value.length) return value;\n return kept.length > 0 ? kept : undefined;\n}\n\n/** Produces the project settings content for a reconciliation, without writing it. */\nexport function mergeProjectPiSettings(\n current: JsonObject,\n repoRoot: string,\n homeDirectory: string = os.homedir(),\n): JsonObject {\n const baseDirectory = projectPiDirectory(repoRoot);\n const merged: JsonObject = { ...current };\n for (const key of [EXTENSIONS_KEY, PACKAGES_KEY]) {\n if (!(key in merged)) continue;\n const next = withoutDoomEntries(merged[key], baseDirectory, homeDirectory);\n if (next === undefined) delete merged[key];\n else merged[key] = next;\n }\n return merged;\n}\n\n/** Reads Pi's project settings, or nothing when the repository declares none. */\nexport function readProjectPiSettings(repoRoot: string): JsonObject | undefined {\n const settingsPath = projectPiSettingsPath(repoRoot);\n if (!fs.existsSync(settingsPath)) return undefined;\n const settings = readJson(settingsPath);\n return isRecord(settings) ? settings : {};\n}\n\nexport function serializeProjectPiSettings(settings: JsonObject): string {\n return `${JSON.stringify(settings, null, 2)}\\n`;\n}\n\n/** True when the repository still registers DoomPi alongside the user scope. */\nexport function projectRegistersDoom(repoRoot: string, homeDirectory: string = os.homedir()): boolean {\n const settings = readProjectPiSettings(repoRoot);\n if (!settings) return false;\n return (\n serializeProjectPiSettings(mergeProjectPiSettings(settings, repoRoot, homeDirectory)) !==\n serializeProjectPiSettings(settings)\n );\n}\n\n/**\n * Removes the alias a repository-scoped registration needed.\n *\n * Only a symbolic link is removed, for the same reason sync refuses to replace\n * one: anything else at that path was not created here.\n */\nfunction removeProjectExtensionAlias(repoRoot: string): void {\n const aliasPath = piExtensionAliasPath(projectPiDirectory(repoRoot));\n if (fs.lstatSync(aliasPath, { throwIfNoEntry: false })?.isSymbolicLink()) {\n fs.rmSync(aliasPath, { force: true });\n }\n}\n\n/**\n * Reconciles the repository's Pi settings with the user-scope registration.\n *\n * The file is never created and never deleted, only rewritten: an absent one\n * means the repository registers nothing, and an existing one is a repository\n * root marker that other DoomPi code walks up to find.\n */\nexport function writeProjectPiSettings(repoRoot: string, homeDirectory: string = os.homedir()): string | undefined {\n const settings = readProjectPiSettings(repoRoot);\n removeProjectExtensionAlias(repoRoot);\n if (!settings) return undefined;\n\n const merged = mergeProjectPiSettings(settings, repoRoot, homeDirectory);\n const serialized = serializeProjectPiSettings(merged);\n if (serialized === serializeProjectPiSettings(settings)) return undefined;\n\n const settingsPath = projectPiSettingsPath(repoRoot);\n writeFileAtomic(settingsPath, serialized);\n return settingsPath;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projectSettings.mjs","names":[],"sources":["../../../src/builders/cli/projectSettings.ts"],"sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { DOOM_PACKAGE_NAME, isDoomPackagePath } from '@agimon-ai/doompi-core/doom-package';\nimport { isRecord, readJson, writeFileAtomic } from '@agimon-ai/doompi-core/runtime-json';\nimport type { JsonObject } from '@agimon-ai/doompi-core/runtime-json';\n\nimport { piExtensionAliasPath } from './piExtensionAlias';\n\n/**\n * Pi's project settings, as `doompi sync` reconciles them.\n *\n * Pi documents project settings as overriding user settings, but resource\n * resolution unions the two scopes and dedupes only by the resolved file's real\n * path. A repository that registers DoomPi alongside the stable user entry\n * therefore loads two installs of the same package against one home-scoped\n * worktree state. Sync removes the redundant project registration and leaves every\n * other key alone: the repository still overrides the user scope, through its own\n * `.doom` configuration and the runtime staged under `~/.pi/.doom/sync`.\n */\n\nconst PI_DIRECTORY = '.pi';\nconst SETTINGS_FILE = 'settings.json';\nconst EXTENSIONS_KEY = 'extensions';\nconst PACKAGES_KEY = 'packages';\nconst PACKAGE_SOURCE_KEY = 'source';\nconst HOME_ALIAS = '~';\nconst HOME_ALIAS_PREFIX = '~/';\n/** Pattern-form entries Pi filters with rather than resolves. */\nconst PATTERN_PREFIXES = ['!', '+', '-'] as const;\n\n/**\n * Reported by `sync --check` and `build`, and repaired by `sync`.\n *\n * Lives here rather than with either command because both report the same\n * finding about the same file, and the detector below is what decides it.\n */\nexport const DUPLICATE_REGISTRATION_DRIFT = 'duplicate DoomPi registration in .pi/settings.json';\n\n/** Pi's project configuration directory, the base its relative paths resolve against. */\nexport function projectPiDirectory(repoRoot: string): string {\n return path.join(repoRoot, PI_DIRECTORY);\n}\n\nexport function projectPiSettingsPath(repoRoot: string): string {\n return path.join(projectPiDirectory(repoRoot), SETTINGS_FILE);\n}\n\nfunction resolveSettingsPath(value: string, baseDirectory: string, homeDirectory: string): string {\n if (value === HOME_ALIAS) return homeDirectory;\n if (value.startsWith(HOME_ALIAS_PREFIX)) return path.join(homeDirectory, value.slice(HOME_ALIAS_PREFIX.length));\n return path.resolve(baseDirectory, value);\n}\n\n/**\n * Whether one settings entry names this package.\n *\n * The bare package name is matched textually because that is the spelling sync\n * writes into the user scope, and a resolved path is matched by walking to the\n * package it belongs to, which is what catches `../packages/cli/doompi` and a\n * direct reference to a file inside it. Pattern entries select among already\n * resolved paths rather than naming one, so they are left for the user to own.\n */\nfunction isDoomEntry(value: string, baseDirectory: string, homeDirectory: string): boolean {\n const normalized = value.replaceAll('\\\\', '/').trim();\n if (normalized === DOOM_PACKAGE_NAME) return true;\n if (PATTERN_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;\n return isDoomPackagePath(resolveSettingsPath(normalized, baseDirectory, homeDirectory));\n}\n\nfunction packageSource(entry: unknown): string | undefined {\n if (typeof entry === 'string') return entry;\n if (isRecord(entry) && typeof entry[PACKAGE_SOURCE_KEY] === 'string') return entry[PACKAGE_SOURCE_KEY];\n return undefined;\n}\n\n/**\n * Drops DoomPi from one resource list, or reports that the key should go.\n *\n * A list that empties out is removed rather than left behind: an empty array is\n * indistinguishable from an absent one to Pi, and keeping it would leave the\n * repository looking like it still configures something.\n */\nfunction withoutDoomEntries(value: unknown, baseDirectory: string, homeDirectory: string): unknown {\n if (!Array.isArray(value)) return value;\n const kept = value.filter((entry) => {\n const source = packageSource(entry);\n return source === undefined || !isDoomEntry(source, baseDirectory, homeDirectory);\n });\n if (kept.length === value.length) return value;\n return kept.length > 0 ? kept : undefined;\n}\n\n/** Produces the project settings content for a reconciliation, without writing it. */\nexport function mergeProjectPiSettings(\n current: JsonObject,\n repoRoot: string,\n homeDirectory: string = os.homedir(),\n): JsonObject {\n const baseDirectory = projectPiDirectory(repoRoot);\n const merged: JsonObject = { ...current };\n for (const key of [EXTENSIONS_KEY, PACKAGES_KEY]) {\n if (!(key in merged)) continue;\n const next = withoutDoomEntries(merged[key], baseDirectory, homeDirectory);\n if (next === undefined) delete merged[key];\n else merged[key] = next;\n }\n return merged;\n}\n\n/** Reads Pi's project settings, or nothing when the repository declares none. */\nexport function readProjectPiSettings(repoRoot: string): JsonObject | undefined {\n const settingsPath = projectPiSettingsPath(repoRoot);\n if (!fs.existsSync(settingsPath)) return undefined;\n const settings = readJson(settingsPath);\n return isRecord(settings) ? settings : {};\n}\n\nexport function serializeProjectPiSettings(settings: JsonObject): string {\n return `${JSON.stringify(settings, null, 2)}\\n`;\n}\n\n/** True when the repository still registers DoomPi alongside the user scope. */\nexport function projectRegistersDoom(repoRoot: string, homeDirectory: string = os.homedir()): boolean {\n const settings = readProjectPiSettings(repoRoot);\n if (!settings) return false;\n return (\n serializeProjectPiSettings(mergeProjectPiSettings(settings, repoRoot, homeDirectory)) !==\n serializeProjectPiSettings(settings)\n );\n}\n\n/**\n * Removes the alias a repository-scoped registration needed.\n *\n * Only a symbolic link is removed, for the same reason sync refuses to replace\n * one: anything else at that path was not created here.\n */\nfunction removeProjectExtensionAlias(repoRoot: string): void {\n const aliasPath = piExtensionAliasPath(projectPiDirectory(repoRoot));\n if (fs.lstatSync(aliasPath, { throwIfNoEntry: false })?.isSymbolicLink()) {\n fs.rmSync(aliasPath, { force: true });\n }\n}\n\n/**\n * Reconciles the repository's Pi settings with the user-scope registration.\n *\n * The file is never created and never deleted, only rewritten: an absent one\n * means the repository registers nothing, and an existing one is a repository\n * root marker that other DoomPi code walks up to find.\n */\nexport function writeProjectPiSettings(repoRoot: string, homeDirectory: string = os.homedir()): string | undefined {\n const settings = readProjectPiSettings(repoRoot);\n removeProjectExtensionAlias(repoRoot);\n if (!settings) return undefined;\n\n const merged = mergeProjectPiSettings(settings, repoRoot, homeDirectory);\n const serialized = serializeProjectPiSettings(merged);\n if (serialized === serializeProjectPiSettings(settings)) return undefined;\n\n const settingsPath = projectPiSettingsPath(repoRoot);\n writeFileAtomic(settingsPath, serialized);\n return settingsPath;\n}\n"],"mappings":"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"}
@@ -1,7 +1,9 @@
1
1
  const require_runtime = require("../../_virtual/_rolldown/runtime.cjs");
2
2
  require("../../composition/harnessState/index.cjs");
3
3
  const require_index$1 = require("../../composition/repository/index.cjs");
4
+ const require_index$2 = require("../../composition/syncState/index.cjs");
4
5
  const require_harnessContext = require("../cli/harnessContext.cjs");
6
+ const require_index$3 = require("../../composition/syncDrift/index.cjs");
5
7
  const require_selectionStatus = require("./selectionStatus.cjs");
6
8
  const require_sessionArguments = require("./sessionArguments.cjs");
7
9
  let node_fs = require("node:fs");
@@ -53,6 +55,10 @@ async function runServerRuntime(options, runtime) {
53
55
  env: baseEnvironment,
54
56
  warn: notice
55
57
  });
58
+ const openSessions = (0, _agimon_ai_doompi_core_history.createOpenSessionRegistry)({
59
+ directory: node_path.default.join((0, _agimon_ai_doompi_core_pi_settings.piAgentDirectory)(baseEnvironment), "server"),
60
+ onNotice: notice
61
+ });
56
62
  let nextEventLoopTick = performance.now() + 1e3;
57
63
  const eventLoopMonitor = setInterval(() => {
58
64
  const now = performance.now();
@@ -78,7 +84,14 @@ async function runServerRuntime(options, runtime) {
78
84
  const pendingSessions = /* @__PURE__ */ new Map();
79
85
  const sessionArtifacts = /* @__PURE__ */ new Map();
80
86
  let mountSessionApis;
87
+ /**
88
+ * Shutdown runs every session through the same close path a user-initiated
89
+ * close uses, so without this the registry would be emptied by the very event
90
+ * it exists to survive.
91
+ */
92
+ let shuttingDown = false;
81
93
  const closeManagedSession = async (sessionId) => {
94
+ if (!shuttingDown) openSessions.remove(sessionId);
82
95
  const artifacts = sessionArtifacts.get(sessionId);
83
96
  sessionArtifacts.delete(sessionId);
84
97
  webCompositions?.remove({
@@ -155,6 +168,7 @@ async function runServerRuntime(options, runtime) {
155
168
  }),
156
169
  createSession: (request) => openSession(request),
157
170
  onNotice: notice,
171
+ hubToken: () => attachToken,
158
172
  requestSessionApi: (scope, request) => requestSessionApi(scope, request)
159
173
  });
160
174
  let harnessContext;
@@ -207,6 +221,20 @@ async function runServerRuntime(options, runtime) {
207
221
  active: hub.snapshot().some((session) => session.workspaceId === workspace.id)
208
222
  })),
209
223
  resolveRepository: (id) => hub.workspaces().find((workspace) => workspace.id === id)?.root,
224
+ readRepositorySync: (id) => {
225
+ const root = hub.workspaces().find((workspace) => workspace.id === id)?.root;
226
+ if (!root) return void 0;
227
+ const drift = require_index$3.readSyncDrift({
228
+ repoRoot: root,
229
+ homeDirectory,
230
+ requireWebBundle: true
231
+ });
232
+ const state = require_index$2.readSyncState(root, homeDirectory);
233
+ return {
234
+ ...drift,
235
+ mcpProjection: state?.fileState.mcpProjection
236
+ };
237
+ },
210
238
  onNotice: notice
211
239
  };
212
240
  const globalBundle = await loadComposition(globalRoot, "global");
@@ -223,9 +251,9 @@ async function runServerRuntime(options, runtime) {
223
251
  headers.set("x-doompi-token", attachToken);
224
252
  return fetch(new URL(`${from.pathname}${from.search}`, cockpit.url), new Request(request, { headers }));
225
253
  },
226
- connectProtocol: () => {
254
+ connectProtocol: (pathname) => {
227
255
  if (!cockpit || !attachToken) throw new Error("The headless protocol listener is not ready.");
228
- const url = new URL("/api/pi", cockpit.url);
256
+ const url = new URL(pathname, cockpit.url);
229
257
  url.protocol = "ws:";
230
258
  return new ws.default(url, { headers: { "x-doompi-token": attachToken } });
231
259
  }
@@ -245,6 +273,12 @@ async function runServerRuntime(options, runtime) {
245
273
  const pending = admissions.get(id);
246
274
  if (pending) return pending;
247
275
  const admission = (async () => {
276
+ const syncEnvironment = {
277
+ ...baseEnvironment,
278
+ DOOMPI_ROOT: root
279
+ };
280
+ for (const key of [_agimon_ai_doompi_config_harnessStore.HARNESS_STATE_POINTER, ...Object.values(_agimon_ai_doompi_config_harnessState.HARNESS_STATE_KEYS)]) delete syncEnvironment[key];
281
+ await syncWorkspace(root, syncEnvironment);
248
282
  const bundle = await loadComposition(root, "workspace");
249
283
  await hub.mountFacets(bundle.facets, {
250
284
  ...sharedApiContext,
@@ -335,6 +369,7 @@ async function runServerRuntime(options, runtime) {
335
369
  directEvents: hub.directEvents,
336
370
  hubToken: token,
337
371
  sessionService: hub.sessionService,
372
+ pluginRegistry: hub.pluginRegistry,
338
373
  apis: [],
339
374
  facets: pendingSessions.get(sessionOptions.sessionId)?.bundle.facets ?? [],
340
375
  workspaceId: sessionOptions.workspaceId,
@@ -362,7 +397,7 @@ async function runServerRuntime(options, runtime) {
362
397
  if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(request.basePath)) return Response.json({ error: "Invalid session API base path." }, { status: 400 });
363
398
  if (!request.path.startsWith("/") || request.path.startsWith("//") || request.path.includes("#")) return Response.json({ error: "Invalid session API path." }, { status: 400 });
364
399
  const body = request.body === null || request.body === void 0 ? void 0 : request.body;
365
- return artifacts.apis.request(new Request(`http://doompi.local/api/plugin/${request.basePath}${request.path}`, {
400
+ return artifacts.apis.request(new Request(`http://doompi.local/api/plugins/${request.basePath}${request.path}`, {
366
401
  method: request.method,
367
402
  headers: request.headers,
368
403
  ...body === void 0 ? {} : { body },
@@ -425,7 +460,16 @@ async function runServerRuntime(options, runtime) {
425
460
  cleanup: () => childContext.cleanup(),
426
461
  bundle
427
462
  });
428
- await hub.create(sessionHostOptions(childContext, bundle, identity));
463
+ const created = await hub.create(sessionHostOptions(childContext, bundle, identity));
464
+ if (created.workspaceId !== void 0) openSessions.add({
465
+ sessionId: created.id,
466
+ workspaceId: created.workspaceId,
467
+ cwd: created.cwd,
468
+ name: created.name,
469
+ createdAt: created.createdAt,
470
+ ...created.parentSessionId === void 0 ? {} : { parentSessionId: created.parentSessionId },
471
+ ...created.sessionProvenance === void 0 ? {} : { sessionProvenance: created.sessionProvenance }
472
+ });
429
473
  return {
430
474
  sessionId: identity.sessionId,
431
475
  cwd: childContext.options.cwd
@@ -478,6 +522,16 @@ async function runServerRuntime(options, runtime) {
478
522
  }, target.id);
479
523
  return target.id;
480
524
  },
525
+ dormantSessions: () => openSessions.list(),
526
+ removeDormantSession: (record) => openSessions.remove(record.sessionId),
527
+ reviveSession: async (record) => {
528
+ await openSession({
529
+ cwd: record.cwd,
530
+ name: record.name,
531
+ ...record.parentSessionId === void 0 ? {} : { parentSessionId: record.parentSessionId },
532
+ ...record.sessionProvenance === void 0 ? {} : { sessionProvenance: record.sessionProvenance }
533
+ }, record.sessionId);
534
+ },
481
535
  requestAsset: (request) => webCompositions?.request(request) ?? Promise.resolve(void 0),
482
536
  compositions: () => ({
483
537
  global: webCompositions?.get({ scope: "global" }),
@@ -494,7 +548,7 @@ async function runServerRuntime(options, runtime) {
494
548
  telemetry,
495
549
  onNotice: notice
496
550
  });
497
- notice(`protocol on ${cockpit.url}/api/pi`);
551
+ notice(`protocol on ${cockpit.url}/api/ws`);
498
552
  let resolveShutdown;
499
553
  const shutdown = new Promise((resolve) => {
500
554
  resolveShutdown = resolve;
@@ -508,6 +562,7 @@ async function runServerRuntime(options, runtime) {
508
562
  signal.removeEventListener("abort", stop);
509
563
  }
510
564
  } finally {
565
+ shuttingDown = true;
511
566
  clearInterval(eventLoopMonitor);
512
567
  await bounded(telemetry.recordEvent("doompi_server.shutdown"), "shutdown telemetry", notice);
513
568
  await Promise.allSettled([cockpit?.close()]);