@agimon-ai/doompi-notification 0.0.1-alpha.2

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 (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  4. package/dist/adapters/pi/extension.cjs +2 -0
  5. package/dist/adapters/pi/extension.cjs.map +1 -0
  6. package/dist/adapters/pi/extension.d.cts +19 -0
  7. package/dist/adapters/pi/extension.d.cts.map +1 -0
  8. package/dist/adapters/pi/extension.d.mts +19 -0
  9. package/dist/adapters/pi/extension.d.mts.map +1 -0
  10. package/dist/adapters/pi/extension.mjs +2 -0
  11. package/dist/adapters/pi/extension.mjs.map +1 -0
  12. package/dist/adapters/shellTitleController.cjs +27 -0
  13. package/dist/adapters/shellTitleController.cjs.map +1 -0
  14. package/dist/adapters/shellTitleController.d.cts +13 -0
  15. package/dist/adapters/shellTitleController.d.cts.map +1 -0
  16. package/dist/adapters/shellTitleController.d.mts +13 -0
  17. package/dist/adapters/shellTitleController.d.mts.map +1 -0
  18. package/dist/adapters/shellTitleController.mjs +27 -0
  19. package/dist/adapters/shellTitleController.mjs.map +1 -0
  20. package/dist/adapters/systemNotification.cjs +2 -0
  21. package/dist/adapters/systemNotification.cjs.map +1 -0
  22. package/dist/adapters/systemNotification.d.cts +14 -0
  23. package/dist/adapters/systemNotification.d.cts.map +1 -0
  24. package/dist/adapters/systemNotification.d.mts +14 -0
  25. package/dist/adapters/systemNotification.d.mts.map +1 -0
  26. package/dist/adapters/systemNotification.mjs +2 -0
  27. package/dist/adapters/systemNotification.mjs.map +1 -0
  28. package/dist/extensions/pi.cjs +1 -0
  29. package/dist/extensions/pi.d.cts +2 -0
  30. package/dist/extensions/pi.d.mts +2 -0
  31. package/dist/extensions/pi.mjs +1 -0
  32. package/dist/index.cjs +1 -0
  33. package/dist/index.d.cts +7 -0
  34. package/dist/index.d.mts +7 -0
  35. package/dist/index.mjs +1 -0
  36. package/dist/services/notificationPolicy.cjs +2 -0
  37. package/dist/services/notificationPolicy.cjs.map +1 -0
  38. package/dist/services/notificationPolicy.d.cts +28 -0
  39. package/dist/services/notificationPolicy.d.cts.map +1 -0
  40. package/dist/services/notificationPolicy.d.mts +28 -0
  41. package/dist/services/notificationPolicy.d.mts.map +1 -0
  42. package/dist/services/notificationPolicy.mjs +2 -0
  43. package/dist/services/notificationPolicy.mjs.map +1 -0
  44. package/dist/services/notificationText.cjs +2 -0
  45. package/dist/services/notificationText.cjs.map +1 -0
  46. package/dist/services/notificationText.d.cts +28 -0
  47. package/dist/services/notificationText.d.cts.map +1 -0
  48. package/dist/services/notificationText.d.mts +28 -0
  49. package/dist/services/notificationText.d.mts.map +1 -0
  50. package/dist/services/notificationText.mjs +2 -0
  51. package/dist/services/notificationText.mjs.map +1 -0
  52. package/dist/types/notifications.d.cts +33 -0
  53. package/dist/types/notifications.d.cts.map +1 -0
  54. package/dist/types/notifications.d.mts +33 -0
  55. package/dist/types/notifications.d.mts.map +1 -0
  56. package/llms.txt +7 -0
  57. package/package.json +84 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vuong Ngo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @agimon-ai/doompi-notification
2
+
3
+ Desktop notifications and an animated shell-tab title for
4
+ [DoomPi](https://www.npmjs.com/package/@agimon-ai/doompi) sessions.
5
+
6
+ Long agent runs are worth walking away from. This package makes that safe: the terminal tab says
7
+ what the session is doing at a glance, and the desktop says when the agent has stopped and why.
8
+
9
+ ## What it does
10
+
11
+ | Surface | Behaviour |
12
+ | ---------------- | ----------------------------------------------------------------------------------------------------- |
13
+ | Shell tab title | `π - <session or first prompt> - <repository>`, with a braille spinner while the agent works |
14
+ | Desktop, mid-run | one notification per dialog the agent opens on its own initiative, and per `ask_user_question` prompt |
15
+ | Desktop, at rest | one notification when a run settles with nothing queued behind it |
16
+
17
+ Notifications go to `cmux notify` first, because it routes them back to the window the session
18
+ lives in. On macOS a missing `cmux` falls back to `osascript`. Any other host stays silent rather
19
+ than failing the turn.
20
+
21
+ ## The shell title
22
+
23
+ The title is animated from a worker thread, so a busy agent turn never stalls the spinner. The
24
+ worker is unreferenced, so a pending frame cannot hold the process open. If worker threads are
25
+ unavailable, or the worker fails or exits, the last command replays on the main thread.
26
+
27
+ Titles are only written to an attached terminal: `rpc` sessions and headless ones get none, so
28
+ escape sequences never end up in machine-read output.
29
+
30
+ ## Quiet by design
31
+
32
+ - A detached subagent (`PI_SUBAGENT_CHILD`) registers nothing. It shares its parent's terminal and
33
+ desktop, and a second voice reporting the same run is noise.
34
+ - Dialogs only notify while the agent holds the turn. A dialog the user opened is already in front
35
+ of them.
36
+ - An `ask_user_question` prompt notifies from its own event, and the dialog it then opens stays
37
+ silent, so one question is announced once.
38
+ - A settled run with pending follow-up messages notifies nothing: the agent is about to keep going.
39
+
40
+ ## Installation
41
+
42
+ DoomPi depends on this package and activates it as fixed host core, so a DoomPi install already has
43
+ it. It is not selectable from `.doom/modes.yaml`.
44
+
45
+ ## License
46
+
47
+ MIT
@@ -0,0 +1 @@
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,s)=>(s=n==null?{}:e(i(n)),o(r||!n||!n.__esModule||!a.call(n,`default`)?t(s,`default`,{value:n,enumerable:!0}):s,n));exports.__toESM=s;
@@ -0,0 +1,2 @@
1
+ const e=require("../../services/notificationPolicy.cjs"),t=require("../../services/notificationText.cjs"),n=require("../shellTitleController.cjs"),r=require("../systemNotification.cjs");let i=require("@agimon-ai/doompi-extension-contracts"),a=require("@agimon-ai/doompi-extension-contracts/child-process"),o=require("@deepseek-ai/cordis");function s(e,t,n){let r=e.confirm.bind(e);e.confirm=(e,i,a)=>(t()&&n(`${e}: ${i}`),r(e,i,a));let i=e.select.bind(e);e.select=(e,r,a)=>(t()&&n(e),i(e,r,a));let a=e.input.bind(e);e.input=(e,r,i)=>(t()&&n(e),a(e,r,i));let o=e.editor.bind(e);e.editor=(e,r)=>(t()&&n(e),o(e,r))}async function c(c,l={}){if((l.environment??process.env)[a.SUBAGENT_CHILD_ENV])return;let u=new o.Context,d,f=()=>(d??=u.fiber.dispose(),d);try{u.effect(function*(){let a=l.titleController??n.createWorkerTitleController(),o=new WeakSet,u=!0,d=!1,f=!1,p,m,h=e=>t.shellTabTitle({cwd:e,sessionName:c.getSessionName(),prompt:p}),g=t=>{if(!(!u||!e.supportsShellTitle(t)))return m={cwd:t.cwd,write:e=>t.ui.setTitle(e)},m},_=e=>{u&&r.sendSystemNotification(c,t.attentionNotification(e))},v=(0,i.subscribeToAskUserEvents)(c.events,{onPrompt(t){_(e.askUserPromptBody(t.questions))},onBlocked(e){f=e.active}});yield()=>v.dispose(),yield()=>{m&&a.dispose(h(m.cwd),m.write),m=void 0},yield()=>{u=!1,d=!1,f=!1},c.on(`input`,(e,n)=>{if(!u||p||e.source===`extension`)return;p=t.promptTitle(e.text);let r=p?g(n):void 0;r&&a.set(h(r.cwd),r.write)}),c.on(`session_start`,(t,n)=>{!u||o.has(n.ui)||(o.add(n.ui),s(n.ui,()=>u&&e.warrantsAttentionNotification({agentRunning:d,askUserBlocked:f}),_))}),c.on(`agent_start`,(e,t)=>{if(!u)return;d=!0;let n=g(t);n&&a.start(h(n.cwd),n.write)}),c.on(`agent_settled`,async(n,i)=>{if(!u)return;d=!1,f=!1;let o=g(i);o&&a.stop(h(o.cwd),o.write),e.warrantsSettledNotification(i.hasPendingMessages())&&await r.sendSystemNotification(c,t.settledNotification({cwd:i.cwd,sessionName:c.getSessionName()}))})},`@agimon-ai/doompi-notification`)}catch(e){throw await f(),e}c.on(`session_shutdown`,f)}exports.default=c,exports.notificationExtension=c;
2
+ //# sourceMappingURL=extension.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.cjs","names":["SUBAGENT_CHILD_ENV","Context","createWorkerTitleController","shellTabTitle","supportsShellTitle","sendSystemNotification","attentionNotification","subscribeToAskUserEvents","askUserPromptBody","promptTitle","warrantsAttentionNotification","warrantsSettledNotification","settledNotification"],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { subscribeToAskUserEvents } from '@agimon-ai/doompi-extension-contracts';\nimport { SUBAGENT_CHILD_ENV } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext, ExtensionUIContext } from '@earendil-works/pi-coding-agent';\nimport {\n askUserPromptBody,\n supportsShellTitle,\n warrantsAttentionNotification,\n warrantsSettledNotification,\n} from '../../services/notificationPolicy.ts';\nimport {\n attentionNotification,\n promptTitle,\n settledNotification,\n shellTabTitle,\n} from '../../services/notificationText.ts';\nimport type { ShellTitleController, WriteTitle } from '../../types/notifications.ts';\nimport { createWorkerTitleController } from '../shellTitleController.ts';\nimport { sendSystemNotification } from '../systemNotification.ts';\n\nconst AGENT_SETTLED_EVENT = 'agent_settled';\nconst AGENT_START_EVENT = 'agent_start';\nconst EXTENSION_INPUT_SOURCE = 'extension';\nconst INPUT_EVENT = 'input';\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-notification';\nconst SESSION_SHUTDOWN_EVENT = 'session_shutdown';\nconst SESSION_START_EVENT = 'session_start';\n\nexport interface NotificationExtensionOptions {\n /** Replaces the worker-thread animator, which a host without worker threads cannot start. */\n titleController?: ShellTitleController;\n environment?: NodeJS.ProcessEnv;\n}\n\n/** Where the tab title is written, remembered so shutdown can restore it without an event. */\ninterface TitleSurface {\n cwd: string;\n write: WriteTitle;\n}\n\n/**\n * Announces every dialog the agent opens on its own initiative.\n *\n * Pi has no event for \"a dialog is showing\", so the only way to catch one is to\n * wrap the UI context the session hands out. Each wrapper delegates to the\n * original, so nothing about the dialog itself changes.\n */\nfunction wrapAttentionDialogs(\n ui: ExtensionUIContext,\n shouldNotify: () => boolean,\n notify: (body: string) => void,\n): void {\n const originalConfirm = ui.confirm.bind(ui);\n ui.confirm = (title, message, options) => {\n if (shouldNotify()) notify(`${title}: ${message}`);\n return originalConfirm(title, message, options);\n };\n\n const originalSelect = ui.select.bind(ui);\n ui.select = (title, options, dialogOptions) => {\n if (shouldNotify()) notify(title);\n return originalSelect(title, options, dialogOptions);\n };\n\n const originalInput = ui.input.bind(ui);\n ui.input = (title, placeholder, options) => {\n if (shouldNotify()) notify(title);\n return originalInput(title, placeholder, options);\n };\n\n const originalEditor = ui.editor.bind(ui);\n ui.editor = (title, prefill) => {\n if (shouldNotify()) notify(title);\n return originalEditor(title, prefill);\n };\n}\n\n/**\n * The package's single standard Pi factory, and the owner of the cordis Context.\n *\n * Pi can reload an extension in-process, so every registration is yielded to the\n * fiber and one dispose on session shutdown unwinds them in reverse: the handlers\n * go quiet, the tab title is restored, then the event subscriptions are dropped.\n */\nexport async function notificationExtension(\n pi: ExtensionAPI,\n options: NotificationExtensionOptions = {},\n): Promise<void> {\n // A detached subagent shares its parent's terminal and desktop, so anything it\n // announced would be a second voice reporting on the same run.\n if ((options.environment ?? process.env)[SUBAGENT_CHILD_ENV]) return;\n\n const cordis = new Context();\n let disposal: Promise<void> | undefined;\n const dispose = (): Promise<void> => {\n disposal ??= cordis.fiber.dispose();\n return disposal;\n };\n\n try {\n cordis.effect(function* () {\n const titles = options.titleController ?? createWorkerTitleController();\n const wrappedUis = new WeakSet<ExtensionUIContext>();\n let active = true;\n let agentRunning = false;\n let askUserBlocked = false;\n let firstPrompt: string | undefined;\n let surface: TitleSurface | undefined;\n\n const idleTitle = (cwd: string): string =>\n shellTabTitle({ cwd, sessionName: pi.getSessionName(), prompt: firstPrompt });\n const titleSurface = (context: ExtensionContext): TitleSurface | undefined => {\n if (!active || !supportsShellTitle(context)) return undefined;\n surface = { cwd: context.cwd, write: (title) => context.ui.setTitle(title) };\n return surface;\n };\n const notifyAttention = (body: string): void => {\n if (active) void sendSystemNotification(pi, attentionNotification(body));\n };\n\n const askUserSubscriptions = subscribeToAskUserEvents(pi.events, {\n onPrompt(prompt) {\n notifyAttention(askUserPromptBody(prompt.questions));\n },\n onBlocked(payload) {\n askUserBlocked = payload.active;\n },\n });\n yield () => askUserSubscriptions.dispose();\n\n // The fiber owns the animator, so the tab lands on its idle title however\n // the session ends — including a reload that fires no settle event.\n yield () => {\n if (surface) titles.dispose(idleTitle(surface.cwd), surface.write);\n surface = undefined;\n };\n yield () => {\n active = false;\n agentRunning = false;\n askUserBlocked = false;\n };\n\n pi.on(INPUT_EVENT, (event, context) => {\n if (!active || firstPrompt || event.source === EXTENSION_INPUT_SOURCE) return;\n\n firstPrompt = promptTitle(event.text);\n const target = firstPrompt ? titleSurface(context) : undefined;\n if (target) titles.set(idleTitle(target.cwd), target.write);\n });\n\n pi.on(SESSION_START_EVENT, (_event, context) => {\n if (!active || wrappedUis.has(context.ui)) return;\n wrappedUis.add(context.ui);\n wrapAttentionDialogs(\n context.ui,\n () => active && warrantsAttentionNotification({ agentRunning, askUserBlocked }),\n notifyAttention,\n );\n });\n\n pi.on(AGENT_START_EVENT, (_event, context) => {\n if (!active) return;\n agentRunning = true;\n const target = titleSurface(context);\n if (target) titles.start(idleTitle(target.cwd), target.write);\n });\n\n pi.on(AGENT_SETTLED_EVENT, async (_event, context) => {\n if (!active) return;\n agentRunning = false;\n askUserBlocked = false;\n const target = titleSurface(context);\n if (target) titles.stop(idleTitle(target.cwd), target.write);\n if (!warrantsSettledNotification(context.hasPendingMessages())) return;\n\n await sendSystemNotification(pi, settledNotification({ cwd: context.cwd, sessionName: pi.getSessionName() }));\n });\n }, PACKAGE_SOURCE);\n } catch (error) {\n await dispose();\n throw error;\n }\n\n pi.on(SESSION_SHUTDOWN_EVENT, dispose);\n}\n\nexport default notificationExtension;\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { ShellTitleController } from "../../types/notifications.cjs";
2
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ //#region src/adapters/pi/extension.d.ts
4
+ interface NotificationExtensionOptions {
5
+ /** Replaces the worker-thread animator, which a host without worker threads cannot start. */
6
+ titleController?: ShellTitleController;
7
+ environment?: NodeJS.ProcessEnv;
8
+ }
9
+ /**
10
+ * The package's single standard Pi factory, and the owner of the cordis Context.
11
+ *
12
+ * Pi can reload an extension in-process, so every registration is yielded to the
13
+ * fiber and one dispose on session shutdown unwinds them in reverse: the handlers
14
+ * go quiet, the tab title is restored, then the event subscriptions are dropped.
15
+ */
16
+ declare function notificationExtension(pi: ExtensionAPI, options?: NotificationExtensionOptions): Promise<void>;
17
+ //#endregion
18
+ export { NotificationExtensionOptions, notificationExtension as default, notificationExtension };
19
+ //# sourceMappingURL=extension.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;UA4BiB;;EAEf,kBAAkB;EAClB,cAAc,OAAO;;;;;;;;;iBAqDD,sBACpB,IAAI,cACJ,UAAS,+BACR"}
@@ -0,0 +1,19 @@
1
+ import { ShellTitleController } from "../../types/notifications.mjs";
2
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ //#region src/adapters/pi/extension.d.ts
4
+ interface NotificationExtensionOptions {
5
+ /** Replaces the worker-thread animator, which a host without worker threads cannot start. */
6
+ titleController?: ShellTitleController;
7
+ environment?: NodeJS.ProcessEnv;
8
+ }
9
+ /**
10
+ * The package's single standard Pi factory, and the owner of the cordis Context.
11
+ *
12
+ * Pi can reload an extension in-process, so every registration is yielded to the
13
+ * fiber and one dispose on session shutdown unwinds them in reverse: the handlers
14
+ * go quiet, the tab title is restored, then the event subscriptions are dropped.
15
+ */
16
+ declare function notificationExtension(pi: ExtensionAPI, options?: NotificationExtensionOptions): Promise<void>;
17
+ //#endregion
18
+ export { NotificationExtensionOptions, notificationExtension as default, notificationExtension };
19
+ //# sourceMappingURL=extension.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;UA4BiB;;EAEf,kBAAkB;EAClB,cAAc,OAAO;;;;;;;;;iBAqDD,sBACpB,IAAI,cACJ,UAAS,+BACR"}
@@ -0,0 +1,2 @@
1
+ import{askUserPromptBody as e,supportsShellTitle as t,warrantsAttentionNotification as n,warrantsSettledNotification as r}from"../../services/notificationPolicy.mjs";import{attentionNotification as i,promptTitle as a,settledNotification as o,shellTabTitle as s}from"../../services/notificationText.mjs";import{createWorkerTitleController as c}from"../shellTitleController.mjs";import{sendSystemNotification as l}from"../systemNotification.mjs";import{subscribeToAskUserEvents as u}from"@agimon-ai/doompi-extension-contracts";import{SUBAGENT_CHILD_ENV as d}from"@agimon-ai/doompi-extension-contracts/child-process";import{Context as f}from"@deepseek-ai/cordis";function p(e,t,n){let r=e.confirm.bind(e);e.confirm=(e,i,a)=>(t()&&n(`${e}: ${i}`),r(e,i,a));let i=e.select.bind(e);e.select=(e,r,a)=>(t()&&n(e),i(e,r,a));let a=e.input.bind(e);e.input=(e,r,i)=>(t()&&n(e),a(e,r,i));let o=e.editor.bind(e);e.editor=(e,r)=>(t()&&n(e),o(e,r))}async function m(m,h={}){if((h.environment??process.env)[d])return;let g=new f,_,v=()=>(_??=g.fiber.dispose(),_);try{g.effect(function*(){let d=h.titleController??c(),f=new WeakSet,g=!0,_=!1,v=!1,y,b,x=e=>s({cwd:e,sessionName:m.getSessionName(),prompt:y}),S=e=>{if(!(!g||!t(e)))return b={cwd:e.cwd,write:t=>e.ui.setTitle(t)},b},C=e=>{g&&l(m,i(e))},w=u(m.events,{onPrompt(t){C(e(t.questions))},onBlocked(e){v=e.active}});yield()=>w.dispose(),yield()=>{b&&d.dispose(x(b.cwd),b.write),b=void 0},yield()=>{g=!1,_=!1,v=!1},m.on(`input`,(e,t)=>{if(!g||y||e.source===`extension`)return;y=a(e.text);let n=y?S(t):void 0;n&&d.set(x(n.cwd),n.write)}),m.on(`session_start`,(e,t)=>{!g||f.has(t.ui)||(f.add(t.ui),p(t.ui,()=>g&&n({agentRunning:_,askUserBlocked:v}),C))}),m.on(`agent_start`,(e,t)=>{if(!g)return;_=!0;let n=S(t);n&&d.start(x(n.cwd),n.write)}),m.on(`agent_settled`,async(e,t)=>{if(!g)return;_=!1,v=!1;let n=S(t);n&&d.stop(x(n.cwd),n.write),r(t.hasPendingMessages())&&await l(m,o({cwd:t.cwd,sessionName:m.getSessionName()}))})},`@agimon-ai/doompi-notification`)}catch(e){throw await v(),e}m.on(`session_shutdown`,v)}export{m as default,m as notificationExtension};
2
+ //# sourceMappingURL=extension.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.mjs","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"sourcesContent":["import { subscribeToAskUserEvents } from '@agimon-ai/doompi-extension-contracts';\nimport { SUBAGENT_CHILD_ENV } from '@agimon-ai/doompi-extension-contracts/child-process';\nimport { Context } from '@deepseek-ai/cordis';\nimport type { ExtensionAPI, ExtensionContext, ExtensionUIContext } from '@earendil-works/pi-coding-agent';\nimport {\n askUserPromptBody,\n supportsShellTitle,\n warrantsAttentionNotification,\n warrantsSettledNotification,\n} from '../../services/notificationPolicy.ts';\nimport {\n attentionNotification,\n promptTitle,\n settledNotification,\n shellTabTitle,\n} from '../../services/notificationText.ts';\nimport type { ShellTitleController, WriteTitle } from '../../types/notifications.ts';\nimport { createWorkerTitleController } from '../shellTitleController.ts';\nimport { sendSystemNotification } from '../systemNotification.ts';\n\nconst AGENT_SETTLED_EVENT = 'agent_settled';\nconst AGENT_START_EVENT = 'agent_start';\nconst EXTENSION_INPUT_SOURCE = 'extension';\nconst INPUT_EVENT = 'input';\nconst PACKAGE_SOURCE = '@agimon-ai/doompi-notification';\nconst SESSION_SHUTDOWN_EVENT = 'session_shutdown';\nconst SESSION_START_EVENT = 'session_start';\n\nexport interface NotificationExtensionOptions {\n /** Replaces the worker-thread animator, which a host without worker threads cannot start. */\n titleController?: ShellTitleController;\n environment?: NodeJS.ProcessEnv;\n}\n\n/** Where the tab title is written, remembered so shutdown can restore it without an event. */\ninterface TitleSurface {\n cwd: string;\n write: WriteTitle;\n}\n\n/**\n * Announces every dialog the agent opens on its own initiative.\n *\n * Pi has no event for \"a dialog is showing\", so the only way to catch one is to\n * wrap the UI context the session hands out. Each wrapper delegates to the\n * original, so nothing about the dialog itself changes.\n */\nfunction wrapAttentionDialogs(\n ui: ExtensionUIContext,\n shouldNotify: () => boolean,\n notify: (body: string) => void,\n): void {\n const originalConfirm = ui.confirm.bind(ui);\n ui.confirm = (title, message, options) => {\n if (shouldNotify()) notify(`${title}: ${message}`);\n return originalConfirm(title, message, options);\n };\n\n const originalSelect = ui.select.bind(ui);\n ui.select = (title, options, dialogOptions) => {\n if (shouldNotify()) notify(title);\n return originalSelect(title, options, dialogOptions);\n };\n\n const originalInput = ui.input.bind(ui);\n ui.input = (title, placeholder, options) => {\n if (shouldNotify()) notify(title);\n return originalInput(title, placeholder, options);\n };\n\n const originalEditor = ui.editor.bind(ui);\n ui.editor = (title, prefill) => {\n if (shouldNotify()) notify(title);\n return originalEditor(title, prefill);\n };\n}\n\n/**\n * The package's single standard Pi factory, and the owner of the cordis Context.\n *\n * Pi can reload an extension in-process, so every registration is yielded to the\n * fiber and one dispose on session shutdown unwinds them in reverse: the handlers\n * go quiet, the tab title is restored, then the event subscriptions are dropped.\n */\nexport async function notificationExtension(\n pi: ExtensionAPI,\n options: NotificationExtensionOptions = {},\n): Promise<void> {\n // A detached subagent shares its parent's terminal and desktop, so anything it\n // announced would be a second voice reporting on the same run.\n if ((options.environment ?? process.env)[SUBAGENT_CHILD_ENV]) return;\n\n const cordis = new Context();\n let disposal: Promise<void> | undefined;\n const dispose = (): Promise<void> => {\n disposal ??= cordis.fiber.dispose();\n return disposal;\n };\n\n try {\n cordis.effect(function* () {\n const titles = options.titleController ?? createWorkerTitleController();\n const wrappedUis = new WeakSet<ExtensionUIContext>();\n let active = true;\n let agentRunning = false;\n let askUserBlocked = false;\n let firstPrompt: string | undefined;\n let surface: TitleSurface | undefined;\n\n const idleTitle = (cwd: string): string =>\n shellTabTitle({ cwd, sessionName: pi.getSessionName(), prompt: firstPrompt });\n const titleSurface = (context: ExtensionContext): TitleSurface | undefined => {\n if (!active || !supportsShellTitle(context)) return undefined;\n surface = { cwd: context.cwd, write: (title) => context.ui.setTitle(title) };\n return surface;\n };\n const notifyAttention = (body: string): void => {\n if (active) void sendSystemNotification(pi, attentionNotification(body));\n };\n\n const askUserSubscriptions = subscribeToAskUserEvents(pi.events, {\n onPrompt(prompt) {\n notifyAttention(askUserPromptBody(prompt.questions));\n },\n onBlocked(payload) {\n askUserBlocked = payload.active;\n },\n });\n yield () => askUserSubscriptions.dispose();\n\n // The fiber owns the animator, so the tab lands on its idle title however\n // the session ends — including a reload that fires no settle event.\n yield () => {\n if (surface) titles.dispose(idleTitle(surface.cwd), surface.write);\n surface = undefined;\n };\n yield () => {\n active = false;\n agentRunning = false;\n askUserBlocked = false;\n };\n\n pi.on(INPUT_EVENT, (event, context) => {\n if (!active || firstPrompt || event.source === EXTENSION_INPUT_SOURCE) return;\n\n firstPrompt = promptTitle(event.text);\n const target = firstPrompt ? titleSurface(context) : undefined;\n if (target) titles.set(idleTitle(target.cwd), target.write);\n });\n\n pi.on(SESSION_START_EVENT, (_event, context) => {\n if (!active || wrappedUis.has(context.ui)) return;\n wrappedUis.add(context.ui);\n wrapAttentionDialogs(\n context.ui,\n () => active && warrantsAttentionNotification({ agentRunning, askUserBlocked }),\n notifyAttention,\n );\n });\n\n pi.on(AGENT_START_EVENT, (_event, context) => {\n if (!active) return;\n agentRunning = true;\n const target = titleSurface(context);\n if (target) titles.start(idleTitle(target.cwd), target.write);\n });\n\n pi.on(AGENT_SETTLED_EVENT, async (_event, context) => {\n if (!active) return;\n agentRunning = false;\n askUserBlocked = false;\n const target = titleSurface(context);\n if (target) titles.stop(idleTitle(target.cwd), target.write);\n if (!warrantsSettledNotification(context.hasPendingMessages())) return;\n\n await sendSystemNotification(pi, settledNotification({ cwd: context.cwd, sessionName: pi.getSessionName() }));\n });\n }, PACKAGE_SOURCE);\n } catch (error) {\n await dispose();\n throw error;\n }\n\n pi.on(SESSION_SHUTDOWN_EVENT, dispose);\n}\n\nexport default notificationExtension;\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ let e=require("node:worker_threads");const t=[`⠋`,`⠙`,`⠹`,`⠸`,`⠼`,`⠴`,`⠦`,`⠧`,`⠇`,`⠏`];function n(){let e=0,n,r=()=>{n&&clearInterval(n),n=void 0,e=0};return{set(e,t){r(),t(e)},start(i,a){r();let o=()=>{a(`${t[e%t.length]} ${i}`),e+=1};o(),n=setInterval(o,80)},stop(e,t){r(),t(e)},dispose(e,t){r(),t(e)}}}function r(){let r=n(),i,a,o,s=()=>{o=void 0,!(!i||!a)&&r[i.action](i.title,a)};try{let n=new e.Worker(`
2
+ const { writeSync } = require('node:fs');
3
+ const { parentPort, workerData } = require('node:worker_threads');
4
+ let frameIndex = 0;
5
+ let timer;
6
+ const writeTitle = (title) => writeSync(1, '\\u001b]0;' + title + '\\u0007');
7
+ const stop = () => {
8
+ if (timer) clearInterval(timer);
9
+ timer = undefined;
10
+ frameIndex = 0;
11
+ };
12
+ parentPort.on('message', (command) => {
13
+ stop();
14
+ if (command.action === 'start') {
15
+ const render = () => {
16
+ writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);
17
+ frameIndex += 1;
18
+ };
19
+ render();
20
+ timer = setInterval(render, workerData.intervalMs);
21
+ return;
22
+ }
23
+ writeTitle(command.title);
24
+ if (command.action === 'dispose') parentPort.close();
25
+ });
26
+ `,{eval:!0,workerData:{frames:t,intervalMs:80}});n.unref(),n.once(`error`,s),n.once(`exit`,e=>{e!==0&&o===n&&s()}),o=n}catch{o=void 0}let c=(e,t)=>{if(i=e,a=t,!o){r[e.action](e.title,t);return}try{o.postMessage(e)}catch{s()}};return{set:(e,t)=>c({action:`set`,title:e},t),start:(e,t)=>c({action:`start`,title:e},t),stop:(e,t)=>c({action:`stop`,title:e},t),dispose:(e,t)=>c({action:`dispose`,title:e},t)}}exports.createMainThreadTitleController=n,exports.createWorkerTitleController=r;
27
+ //# sourceMappingURL=shellTitleController.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shellTitleController.cjs","names":["Worker"],"sources":["../../src/adapters/shellTitleController.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads';\nimport type { ShellTitleCommand, ShellTitleController, WriteTitle } from '../types/notifications.ts';\n\nconst BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\nconst TITLE_FRAME_INTERVAL_MS = 80;\n/**\n * The animator runs off the main thread so a busy agent turn cannot stall the\n * spinner. It is an inline eval source rather than a shipped file because the\n * package is bundled per entry, and a separate worker module would have to be\n * located on disk at runtime.\n */\nconst TITLE_WORKER_SOURCE = `\nconst { writeSync } = require('node:fs');\nconst { parentPort, workerData } = require('node:worker_threads');\nlet frameIndex = 0;\nlet timer;\nconst writeTitle = (title) => writeSync(1, '\\\\u001b]0;' + title + '\\\\u0007');\nconst stop = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n frameIndex = 0;\n};\nparentPort.on('message', (command) => {\n stop();\n if (command.action === 'start') {\n const render = () => {\n writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);\n frameIndex += 1;\n };\n render();\n timer = setInterval(render, workerData.intervalMs);\n return;\n }\n writeTitle(command.title);\n if (command.action === 'dispose') parentPort.close();\n});\n`;\n\n/** Animates on the main thread, for hosts where a worker could not be started. */\nexport function createMainThreadTitleController(): ShellTitleController {\n let frameIndex = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const stopAnimation = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n frameIndex = 0;\n };\n\n return {\n set(title, write) {\n stopAnimation();\n write(title);\n },\n start(title, write) {\n stopAnimation();\n const render = () => {\n write(`${BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]} ${title}`);\n frameIndex += 1;\n };\n render();\n timer = setInterval(render, TITLE_FRAME_INTERVAL_MS);\n },\n stop(title, write) {\n stopAnimation();\n write(title);\n },\n dispose(title, write) {\n stopAnimation();\n write(title);\n },\n };\n}\n\n/**\n * Animates from a worker thread, replaying the last command on the main thread\n * if the worker never started, failed, or exited under us. The worker is\n * unreferenced so a pending frame never holds the process open.\n */\nexport function createWorkerTitleController(): ShellTitleController {\n const fallback = createMainThreadTitleController();\n let lastCommand: ShellTitleCommand | undefined;\n let lastWrite: WriteTitle | undefined;\n let worker: Worker | undefined;\n\n const fallBack = () => {\n worker = undefined;\n if (!lastCommand || !lastWrite) return;\n fallback[lastCommand.action](lastCommand.title, lastWrite);\n };\n\n try {\n const titleWorker = new Worker(TITLE_WORKER_SOURCE, {\n eval: true,\n workerData: { frames: BRAILLE_FRAMES, intervalMs: TITLE_FRAME_INTERVAL_MS },\n });\n titleWorker.unref();\n titleWorker.once('error', fallBack);\n titleWorker.once('exit', (code) => {\n if (code !== 0 && worker === titleWorker) fallBack();\n });\n worker = titleWorker;\n } catch {\n // Workers are unavailable on this host, which the main-thread animator covers.\n worker = undefined;\n }\n\n const dispatch = (command: ShellTitleCommand, write: WriteTitle) => {\n lastCommand = command;\n lastWrite = write;\n if (!worker) {\n fallback[command.action](command.title, write);\n return;\n }\n try {\n worker.postMessage(command);\n } catch {\n // The worker closed between the last frame and this command.\n fallBack();\n }\n };\n\n return {\n set: (title, write) => dispatch({ action: 'set', title }, write),\n start: (title, write) => dispatch({ action: 'start', title }, write),\n stop: (title, write) => dispatch({ action: 'stop', title }, write),\n dispose: (title, write) => dispatch({ action: 'dispose', title }, write),\n };\n}\n"],"mappings":"qCAGA,MAAM,EAAiB,CAAC,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,GAAG,EAoCxE,SAAgB,GAAwD,CACtE,IAAI,EAAa,EACb,EACE,MAAsB,CACtB,GAAO,cAAc,CAAK,EAC9B,EAAQ,IAAA,GACR,EAAa,CACf,EAEA,MAAO,CACL,IAAI,EAAO,EAAO,CAChB,EAAc,EACd,EAAM,CAAK,CACb,EACA,MAAM,EAAO,EAAO,CAClB,EAAc,EACd,IAAM,MAAe,CACnB,EAAM,GAAG,EAAe,EAAa,EAAe,QAAQ,GAAG,GAAO,EACtE,GAAc,CAChB,EACA,EAAO,EACP,EAAQ,YAAY,EAAQ,EAAuB,CACrD,EACA,KAAK,EAAO,EAAO,CACjB,EAAc,EACd,EAAM,CAAK,CACb,EACA,QAAQ,EAAO,EAAO,CACpB,EAAc,EACd,EAAM,CAAK,CACb,CACF,CACF,CAOA,SAAgB,GAAoD,CAClE,IAAM,EAAW,EAAgC,EAC7C,EACA,EACA,EAEE,MAAiB,CACrB,EAAS,IAAA,GACL,GAAC,GAAe,CAAC,IACrB,EAAS,EAAY,OAAO,CAAC,EAAY,MAAO,CAAS,CAC3D,EAEA,GAAI,CACF,IAAM,EAAc,IAAIA,EAAAA,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;EAAqB,CAClD,KAAM,GACN,WAAY,CAAE,OAAQ,EAAgB,WAAY,EAAwB,CAC5E,CAAC,EACD,EAAY,MAAM,EAClB,EAAY,KAAK,QAAS,CAAQ,EAClC,EAAY,KAAK,OAAS,GAAS,CAC7B,IAAS,GAAK,IAAW,GAAa,EAAS,CACrD,CAAC,EACD,EAAS,CACX,MAAQ,CAEN,EAAS,IAAA,EACX,CAEA,IAAM,GAAY,EAA4B,IAAsB,CAGlE,GAFA,EAAc,EACd,EAAY,EACR,CAAC,EAAQ,CACX,EAAS,EAAQ,OAAO,CAAC,EAAQ,MAAO,CAAK,EAC7C,MACF,CACA,GAAI,CACF,EAAO,YAAY,CAAO,CAC5B,MAAQ,CAEN,EAAS,CACX,CACF,EAEA,MAAO,CACL,KAAM,EAAO,IAAU,EAAS,CAAE,OAAQ,MAAO,OAAM,EAAG,CAAK,EAC/D,OAAQ,EAAO,IAAU,EAAS,CAAE,OAAQ,QAAS,OAAM,EAAG,CAAK,EACnE,MAAO,EAAO,IAAU,EAAS,CAAE,OAAQ,OAAQ,OAAM,EAAG,CAAK,EACjE,SAAU,EAAO,IAAU,EAAS,CAAE,OAAQ,UAAW,OAAM,EAAG,CAAK,CACzE,CACF"}
@@ -0,0 +1,13 @@
1
+ import { ShellTitleController } from "../types/notifications.cjs";
2
+ //#region src/adapters/shellTitleController.d.ts
3
+ /** Animates on the main thread, for hosts where a worker could not be started. */
4
+ declare function createMainThreadTitleController(): ShellTitleController;
5
+ /**
6
+ * Animates from a worker thread, replaying the last command on the main thread
7
+ * if the worker never started, failed, or exited under us. The worker is
8
+ * unreferenced so a pending frame never holds the process open.
9
+ */
10
+ declare function createWorkerTitleController(): ShellTitleController;
11
+ //#endregion
12
+ export { createMainThreadTitleController, createWorkerTitleController };
13
+ //# sourceMappingURL=shellTitleController.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shellTitleController.d.cts","names":[],"sources":["../../src/adapters/shellTitleController.ts"],"mappings":";;;iBAuCgB,mCAAmC;;;;;;iBAuCnC,+BAA+B"}
@@ -0,0 +1,13 @@
1
+ import { ShellTitleController } from "../types/notifications.mjs";
2
+ //#region src/adapters/shellTitleController.d.ts
3
+ /** Animates on the main thread, for hosts where a worker could not be started. */
4
+ declare function createMainThreadTitleController(): ShellTitleController;
5
+ /**
6
+ * Animates from a worker thread, replaying the last command on the main thread
7
+ * if the worker never started, failed, or exited under us. The worker is
8
+ * unreferenced so a pending frame never holds the process open.
9
+ */
10
+ declare function createWorkerTitleController(): ShellTitleController;
11
+ //#endregion
12
+ export { createMainThreadTitleController, createWorkerTitleController };
13
+ //# sourceMappingURL=shellTitleController.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shellTitleController.d.mts","names":[],"sources":["../../src/adapters/shellTitleController.ts"],"mappings":";;;iBAuCgB,mCAAmC;;;;;;iBAuCnC,+BAA+B"}
@@ -0,0 +1,27 @@
1
+ import{Worker as e}from"node:worker_threads";const t=[`⠋`,`⠙`,`⠹`,`⠸`,`⠼`,`⠴`,`⠦`,`⠧`,`⠇`,`⠏`];function n(){let e=0,n,r=()=>{n&&clearInterval(n),n=void 0,e=0};return{set(e,t){r(),t(e)},start(i,a){r();let o=()=>{a(`${t[e%t.length]} ${i}`),e+=1};o(),n=setInterval(o,80)},stop(e,t){r(),t(e)},dispose(e,t){r(),t(e)}}}function r(){let r=n(),i,a,o,s=()=>{o=void 0,!(!i||!a)&&r[i.action](i.title,a)};try{let n=new e(`
2
+ const { writeSync } = require('node:fs');
3
+ const { parentPort, workerData } = require('node:worker_threads');
4
+ let frameIndex = 0;
5
+ let timer;
6
+ const writeTitle = (title) => writeSync(1, '\\u001b]0;' + title + '\\u0007');
7
+ const stop = () => {
8
+ if (timer) clearInterval(timer);
9
+ timer = undefined;
10
+ frameIndex = 0;
11
+ };
12
+ parentPort.on('message', (command) => {
13
+ stop();
14
+ if (command.action === 'start') {
15
+ const render = () => {
16
+ writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);
17
+ frameIndex += 1;
18
+ };
19
+ render();
20
+ timer = setInterval(render, workerData.intervalMs);
21
+ return;
22
+ }
23
+ writeTitle(command.title);
24
+ if (command.action === 'dispose') parentPort.close();
25
+ });
26
+ `,{eval:!0,workerData:{frames:t,intervalMs:80}});n.unref(),n.once(`error`,s),n.once(`exit`,e=>{e!==0&&o===n&&s()}),o=n}catch{o=void 0}let c=(e,t)=>{if(i=e,a=t,!o){r[e.action](e.title,t);return}try{o.postMessage(e)}catch{s()}};return{set:(e,t)=>c({action:`set`,title:e},t),start:(e,t)=>c({action:`start`,title:e},t),stop:(e,t)=>c({action:`stop`,title:e},t),dispose:(e,t)=>c({action:`dispose`,title:e},t)}}export{n as createMainThreadTitleController,r as createWorkerTitleController};
27
+ //# sourceMappingURL=shellTitleController.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shellTitleController.mjs","names":[],"sources":["../../src/adapters/shellTitleController.ts"],"sourcesContent":["import { Worker } from 'node:worker_threads';\nimport type { ShellTitleCommand, ShellTitleController, WriteTitle } from '../types/notifications.ts';\n\nconst BRAILLE_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\nconst TITLE_FRAME_INTERVAL_MS = 80;\n/**\n * The animator runs off the main thread so a busy agent turn cannot stall the\n * spinner. It is an inline eval source rather than a shipped file because the\n * package is bundled per entry, and a separate worker module would have to be\n * located on disk at runtime.\n */\nconst TITLE_WORKER_SOURCE = `\nconst { writeSync } = require('node:fs');\nconst { parentPort, workerData } = require('node:worker_threads');\nlet frameIndex = 0;\nlet timer;\nconst writeTitle = (title) => writeSync(1, '\\\\u001b]0;' + title + '\\\\u0007');\nconst stop = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n frameIndex = 0;\n};\nparentPort.on('message', (command) => {\n stop();\n if (command.action === 'start') {\n const render = () => {\n writeTitle(workerData.frames[frameIndex % workerData.frames.length] + ' ' + command.title);\n frameIndex += 1;\n };\n render();\n timer = setInterval(render, workerData.intervalMs);\n return;\n }\n writeTitle(command.title);\n if (command.action === 'dispose') parentPort.close();\n});\n`;\n\n/** Animates on the main thread, for hosts where a worker could not be started. */\nexport function createMainThreadTitleController(): ShellTitleController {\n let frameIndex = 0;\n let timer: ReturnType<typeof setInterval> | undefined;\n const stopAnimation = () => {\n if (timer) clearInterval(timer);\n timer = undefined;\n frameIndex = 0;\n };\n\n return {\n set(title, write) {\n stopAnimation();\n write(title);\n },\n start(title, write) {\n stopAnimation();\n const render = () => {\n write(`${BRAILLE_FRAMES[frameIndex % BRAILLE_FRAMES.length]} ${title}`);\n frameIndex += 1;\n };\n render();\n timer = setInterval(render, TITLE_FRAME_INTERVAL_MS);\n },\n stop(title, write) {\n stopAnimation();\n write(title);\n },\n dispose(title, write) {\n stopAnimation();\n write(title);\n },\n };\n}\n\n/**\n * Animates from a worker thread, replaying the last command on the main thread\n * if the worker never started, failed, or exited under us. The worker is\n * unreferenced so a pending frame never holds the process open.\n */\nexport function createWorkerTitleController(): ShellTitleController {\n const fallback = createMainThreadTitleController();\n let lastCommand: ShellTitleCommand | undefined;\n let lastWrite: WriteTitle | undefined;\n let worker: Worker | undefined;\n\n const fallBack = () => {\n worker = undefined;\n if (!lastCommand || !lastWrite) return;\n fallback[lastCommand.action](lastCommand.title, lastWrite);\n };\n\n try {\n const titleWorker = new Worker(TITLE_WORKER_SOURCE, {\n eval: true,\n workerData: { frames: BRAILLE_FRAMES, intervalMs: TITLE_FRAME_INTERVAL_MS },\n });\n titleWorker.unref();\n titleWorker.once('error', fallBack);\n titleWorker.once('exit', (code) => {\n if (code !== 0 && worker === titleWorker) fallBack();\n });\n worker = titleWorker;\n } catch {\n // Workers are unavailable on this host, which the main-thread animator covers.\n worker = undefined;\n }\n\n const dispatch = (command: ShellTitleCommand, write: WriteTitle) => {\n lastCommand = command;\n lastWrite = write;\n if (!worker) {\n fallback[command.action](command.title, write);\n return;\n }\n try {\n worker.postMessage(command);\n } catch {\n // The worker closed between the last frame and this command.\n fallBack();\n }\n };\n\n return {\n set: (title, write) => dispatch({ action: 'set', title }, write),\n start: (title, write) => dispatch({ action: 'start', title }, write),\n stop: (title, write) => dispatch({ action: 'stop', title }, write),\n dispose: (title, write) => dispatch({ action: 'dispose', title }, write),\n };\n}\n"],"mappings":"6CAGA,MAAM,EAAiB,CAAC,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,IAAK,GAAG,EAoCxE,SAAgB,GAAwD,CACtE,IAAI,EAAa,EACb,EACE,MAAsB,CACtB,GAAO,cAAc,CAAK,EAC9B,EAAQ,IAAA,GACR,EAAa,CACf,EAEA,MAAO,CACL,IAAI,EAAO,EAAO,CAChB,EAAc,EACd,EAAM,CAAK,CACb,EACA,MAAM,EAAO,EAAO,CAClB,EAAc,EACd,IAAM,MAAe,CACnB,EAAM,GAAG,EAAe,EAAa,EAAe,QAAQ,GAAG,GAAO,EACtE,GAAc,CAChB,EACA,EAAO,EACP,EAAQ,YAAY,EAAQ,EAAuB,CACrD,EACA,KAAK,EAAO,EAAO,CACjB,EAAc,EACd,EAAM,CAAK,CACb,EACA,QAAQ,EAAO,EAAO,CACpB,EAAc,EACd,EAAM,CAAK,CACb,CACF,CACF,CAOA,SAAgB,GAAoD,CAClE,IAAM,EAAW,EAAgC,EAC7C,EACA,EACA,EAEE,MAAiB,CACrB,EAAS,IAAA,GACL,GAAC,GAAe,CAAC,IACrB,EAAS,EAAY,OAAO,CAAC,EAAY,MAAO,CAAS,CAC3D,EAEA,GAAI,CACF,IAAM,EAAc,IAAI,EAAO;;;;;;;;;;;;;;;;;;;;;;;;;EAAqB,CAClD,KAAM,GACN,WAAY,CAAE,OAAQ,EAAgB,WAAY,EAAwB,CAC5E,CAAC,EACD,EAAY,MAAM,EAClB,EAAY,KAAK,QAAS,CAAQ,EAClC,EAAY,KAAK,OAAS,GAAS,CAC7B,IAAS,GAAK,IAAW,GAAa,EAAS,CACrD,CAAC,EACD,EAAS,CACX,MAAQ,CAEN,EAAS,IAAA,EACX,CAEA,IAAM,GAAY,EAA4B,IAAsB,CAGlE,GAFA,EAAc,EACd,EAAY,EACR,CAAC,EAAQ,CACX,EAAS,EAAQ,OAAO,CAAC,EAAQ,MAAO,CAAK,EAC7C,MACF,CACA,GAAI,CACF,EAAO,YAAY,CAAO,CAC5B,MAAQ,CAEN,EAAS,CACX,CACF,EAEA,MAAO,CACL,KAAM,EAAO,IAAU,EAAS,CAAE,OAAQ,MAAO,OAAM,EAAG,CAAK,EAC/D,OAAQ,EAAO,IAAU,EAAS,CAAE,OAAQ,QAAS,OAAM,EAAG,CAAK,EACnE,MAAO,EAAO,IAAU,EAAS,CAAE,OAAQ,OAAQ,OAAM,EAAG,CAAK,EACjE,SAAU,EAAO,IAAU,EAAS,CAAE,OAAQ,UAAW,OAAM,EAAG,CAAK,CACzE,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=require("../services/notificationText.cjs");async function t(e,t,n){try{return(await e.exec(t,n,{timeout:3e3})).code===0}catch{return!1}}async function n(n,r,i=process.platform){let a=e.notificationBody(r.body);await t(n,`cmux`,[`notify`,`--title`,r.title,`--subtitle`,r.subtitle,`--body`,a])||i!==`darwin`||await t(n,`osascript`,[`-e`,`on run argv`,`-e`,`display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)`,`-e`,`end run`,r.title,r.subtitle,a])}exports.sendSystemNotification=n;
2
+ //# sourceMappingURL=systemNotification.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemNotification.cjs","names":["notificationBody"],"sources":["../../src/adapters/systemNotification.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { notificationBody } from '../services/notificationText.ts';\nimport type { DesktopNotification } from '../types/notifications.ts';\n\nconst CMUX_COMMAND = 'cmux';\nconst COMMAND_TIMEOUT_MS = 3_000;\nconst DARWIN_PLATFORM = 'darwin';\nconst OSASCRIPT_COMMAND = 'osascript';\nconst OSASCRIPT_EXPRESSION_FLAG = '-e';\n\nasync function execute(pi: ExtensionAPI, command: string, args: string[]): Promise<boolean> {\n try {\n const result = await pi.exec(command, args, { timeout: COMMAND_TIMEOUT_MS });\n return result.code === 0;\n } catch {\n // A notifier that is not installed is the ordinary case, not a fault: report\n // \"not delivered\" and let the caller decide whether another one exists.\n return false;\n }\n}\n\n/**\n * Delivers one desktop notification on a best-effort basis.\n *\n * cmux is tried first because it routes the notification to the window the\n * session actually lives in. Only macOS has a second option worth attempting,\n * and a host with neither stays silent rather than failing the turn.\n */\nexport async function sendSystemNotification(\n pi: ExtensionAPI,\n notification: DesktopNotification,\n platform: string = process.platform,\n): Promise<void> {\n const body = notificationBody(notification.body);\n const sentToCmux = await execute(pi, CMUX_COMMAND, [\n 'notify',\n '--title',\n notification.title,\n '--subtitle',\n notification.subtitle,\n '--body',\n body,\n ]);\n if (sentToCmux || platform !== DARWIN_PLATFORM) return;\n\n await execute(pi, OSASCRIPT_COMMAND, [\n OSASCRIPT_EXPRESSION_FLAG,\n 'on run argv',\n OSASCRIPT_EXPRESSION_FLAG,\n 'display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)',\n OSASCRIPT_EXPRESSION_FLAG,\n 'end run',\n notification.title,\n notification.subtitle,\n body,\n ]);\n}\n"],"mappings":"oDAUA,eAAe,EAAQ,EAAkB,EAAiB,EAAkC,CAC1F,GAAI,CAEF,OAAO,MADc,EAAG,KAAK,EAAS,EAAM,CAAE,QAAS,GAAmB,CAAC,EAAA,CAC7D,OAAS,CACzB,MAAQ,CAGN,MAAO,EACT,CACF,CASA,eAAsB,EACpB,EACA,EACA,EAAmB,QAAQ,SACZ,CACf,IAAM,EAAOA,EAAAA,iBAAiB,EAAa,IAAI,EAU3C,MATqB,EAAQ,EAAI,OAAc,CACjD,SACA,UACA,EAAa,MACb,aACA,EAAa,SACb,SACA,CACF,CAAC,GACiB,IAAa,UAE/B,MAAM,EAAQ,EAAI,YAAmB,CACnC,KACA,cACA,KACA,8FACA,KACA,UACA,EAAa,MACb,EAAa,SACb,CACF,CAAC,CACH"}
@@ -0,0 +1,14 @@
1
+ import { DesktopNotification } from "../types/notifications.cjs";
2
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ //#region src/adapters/systemNotification.d.ts
4
+ /**
5
+ * Delivers one desktop notification on a best-effort basis.
6
+ *
7
+ * cmux is tried first because it routes the notification to the window the
8
+ * session actually lives in. Only macOS has a second option worth attempting,
9
+ * and a host with neither stays silent rather than failing the turn.
10
+ */
11
+ declare function sendSystemNotification(pi: ExtensionAPI, notification: DesktopNotification, platform?: string): Promise<void>;
12
+ //#endregion
13
+ export { sendSystemNotification };
14
+ //# sourceMappingURL=systemNotification.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemNotification.d.cts","names":[],"sources":["../../src/adapters/systemNotification.ts"],"mappings":";;;;;;;;;;iBA4BsB,uBACpB,IAAI,cACJ,cAAc,qBACd,oBACC"}
@@ -0,0 +1,14 @@
1
+ import { DesktopNotification } from "../types/notifications.mjs";
2
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
+ //#region src/adapters/systemNotification.d.ts
4
+ /**
5
+ * Delivers one desktop notification on a best-effort basis.
6
+ *
7
+ * cmux is tried first because it routes the notification to the window the
8
+ * session actually lives in. Only macOS has a second option worth attempting,
9
+ * and a host with neither stays silent rather than failing the turn.
10
+ */
11
+ declare function sendSystemNotification(pi: ExtensionAPI, notification: DesktopNotification, platform?: string): Promise<void>;
12
+ //#endregion
13
+ export { sendSystemNotification };
14
+ //# sourceMappingURL=systemNotification.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemNotification.d.mts","names":[],"sources":["../../src/adapters/systemNotification.ts"],"mappings":";;;;;;;;;;iBA4BsB,uBACpB,IAAI,cACJ,cAAc,qBACd,oBACC"}
@@ -0,0 +1,2 @@
1
+ import{notificationBody as e}from"../services/notificationText.mjs";async function t(e,t,n){try{return(await e.exec(t,n,{timeout:3e3})).code===0}catch{return!1}}async function n(n,r,i=process.platform){let a=e(r.body);await t(n,`cmux`,[`notify`,`--title`,r.title,`--subtitle`,r.subtitle,`--body`,a])||i!==`darwin`||await t(n,`osascript`,[`-e`,`on run argv`,`-e`,`display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)`,`-e`,`end run`,r.title,r.subtitle,a])}export{n as sendSystemNotification};
2
+ //# sourceMappingURL=systemNotification.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemNotification.mjs","names":[],"sources":["../../src/adapters/systemNotification.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { notificationBody } from '../services/notificationText.ts';\nimport type { DesktopNotification } from '../types/notifications.ts';\n\nconst CMUX_COMMAND = 'cmux';\nconst COMMAND_TIMEOUT_MS = 3_000;\nconst DARWIN_PLATFORM = 'darwin';\nconst OSASCRIPT_COMMAND = 'osascript';\nconst OSASCRIPT_EXPRESSION_FLAG = '-e';\n\nasync function execute(pi: ExtensionAPI, command: string, args: string[]): Promise<boolean> {\n try {\n const result = await pi.exec(command, args, { timeout: COMMAND_TIMEOUT_MS });\n return result.code === 0;\n } catch {\n // A notifier that is not installed is the ordinary case, not a fault: report\n // \"not delivered\" and let the caller decide whether another one exists.\n return false;\n }\n}\n\n/**\n * Delivers one desktop notification on a best-effort basis.\n *\n * cmux is tried first because it routes the notification to the window the\n * session actually lives in. Only macOS has a second option worth attempting,\n * and a host with neither stays silent rather than failing the turn.\n */\nexport async function sendSystemNotification(\n pi: ExtensionAPI,\n notification: DesktopNotification,\n platform: string = process.platform,\n): Promise<void> {\n const body = notificationBody(notification.body);\n const sentToCmux = await execute(pi, CMUX_COMMAND, [\n 'notify',\n '--title',\n notification.title,\n '--subtitle',\n notification.subtitle,\n '--body',\n body,\n ]);\n if (sentToCmux || platform !== DARWIN_PLATFORM) return;\n\n await execute(pi, OSASCRIPT_COMMAND, [\n OSASCRIPT_EXPRESSION_FLAG,\n 'on run argv',\n OSASCRIPT_EXPRESSION_FLAG,\n 'display notification (item 3 of argv) with title (item 1 of argv) subtitle (item 2 of argv)',\n OSASCRIPT_EXPRESSION_FLAG,\n 'end run',\n notification.title,\n notification.subtitle,\n body,\n ]);\n}\n"],"mappings":"oEAUA,eAAe,EAAQ,EAAkB,EAAiB,EAAkC,CAC1F,GAAI,CAEF,OAAO,MADc,EAAG,KAAK,EAAS,EAAM,CAAE,QAAS,GAAmB,CAAC,EAAA,CAC7D,OAAS,CACzB,MAAQ,CAGN,MAAO,EACT,CACF,CASA,eAAsB,EACpB,EACA,EACA,EAAmB,QAAQ,SACZ,CACf,IAAM,EAAO,EAAiB,EAAa,IAAI,EAU3C,MATqB,EAAQ,EAAI,OAAc,CACjD,SACA,UACA,EAAa,MACb,aACA,EAAa,SACb,SACA,CACF,CAAC,GACiB,IAAa,UAE/B,MAAM,EAAQ,EAAI,YAAmB,CACnC,KACA,cACA,KACA,8FACA,KACA,UACA,EAAa,MACb,EAAa,SACb,CACF,CAAC,CACH"}
@@ -0,0 +1 @@
1
+ Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:`Module`}});const e=require("../adapters/pi/extension.cjs");exports.default=e.default,exports.notificationExtension=e.default;
@@ -0,0 +1,2 @@
1
+ import notificationExtension from "../adapters/pi/extension.cjs";
2
+ export { notificationExtension as default, notificationExtension };
@@ -0,0 +1,2 @@
1
+ import notificationExtension from "../adapters/pi/extension.mjs";
2
+ export { notificationExtension as default, notificationExtension };
@@ -0,0 +1 @@
1
+ import e from"../adapters/pi/extension.mjs";export{e as default,e as notificationExtension};
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/notificationPolicy.cjs"),t=require("./services/notificationText.cjs"),n=require("./adapters/shellTitleController.cjs"),r=require("./adapters/systemNotification.cjs"),i=require("./adapters/pi/extension.cjs");exports.askUserPromptBody=e.askUserPromptBody,exports.attentionNotification=t.attentionNotification,exports.createMainThreadTitleController=n.createMainThreadTitleController,exports.createWorkerTitleController=n.createWorkerTitleController,exports.notificationBody=t.notificationBody,exports.notificationExtension=i.default,exports.promptTitle=t.promptTitle,exports.sendSystemNotification=r.sendSystemNotification,exports.settledNotification=t.settledNotification,exports.shellTabTitle=t.shellTabTitle,exports.supportsShellTitle=e.supportsShellTitle,exports.warrantsAttentionNotification=e.warrantsAttentionNotification,exports.warrantsSettledNotification=e.warrantsSettledNotification;
@@ -0,0 +1,7 @@
1
+ import { DesktopNotification, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle } from "./types/notifications.cjs";
2
+ import notificationExtension, { NotificationExtensionOptions } from "./adapters/pi/extension.cjs";
3
+ import { createMainThreadTitleController, createWorkerTitleController } from "./adapters/shellTitleController.cjs";
4
+ import { sendSystemNotification } from "./adapters/systemNotification.cjs";
5
+ import { AttentionState, ShellSurface, askUserPromptBody, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification } from "./services/notificationPolicy.cjs";
6
+ import { ShellTabTitleInput, attentionNotification, notificationBody, promptTitle, settledNotification, shellTabTitle } from "./services/notificationText.cjs";
7
+ export { type AttentionState, type DesktopNotification, type NotificationExtensionOptions, type ShellSurface, type ShellTabTitleInput, type ShellTitleAction, type ShellTitleCommand, type ShellTitleController, type WriteTitle, askUserPromptBody, attentionNotification, createMainThreadTitleController, createWorkerTitleController, notificationBody, notificationExtension, promptTitle, sendSystemNotification, settledNotification, shellTabTitle, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
@@ -0,0 +1,7 @@
1
+ import { DesktopNotification, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle } from "./types/notifications.mjs";
2
+ import notificationExtension, { NotificationExtensionOptions } from "./adapters/pi/extension.mjs";
3
+ import { createMainThreadTitleController, createWorkerTitleController } from "./adapters/shellTitleController.mjs";
4
+ import { sendSystemNotification } from "./adapters/systemNotification.mjs";
5
+ import { AttentionState, ShellSurface, askUserPromptBody, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification } from "./services/notificationPolicy.mjs";
6
+ import { ShellTabTitleInput, attentionNotification, notificationBody, promptTitle, settledNotification, shellTabTitle } from "./services/notificationText.mjs";
7
+ export { type AttentionState, type DesktopNotification, type NotificationExtensionOptions, type ShellSurface, type ShellTabTitleInput, type ShellTitleAction, type ShellTitleCommand, type ShellTitleController, type WriteTitle, askUserPromptBody, attentionNotification, createMainThreadTitleController, createWorkerTitleController, notificationBody, notificationExtension, promptTitle, sendSystemNotification, settledNotification, shellTabTitle, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import{askUserPromptBody as e,supportsShellTitle as t,warrantsAttentionNotification as n,warrantsSettledNotification as r}from"./services/notificationPolicy.mjs";import{attentionNotification as i,notificationBody as a,promptTitle as o,settledNotification as s,shellTabTitle as c}from"./services/notificationText.mjs";import{createMainThreadTitleController as l,createWorkerTitleController as u}from"./adapters/shellTitleController.mjs";import{sendSystemNotification as d}from"./adapters/systemNotification.mjs";import f from"./adapters/pi/extension.mjs";export{e as askUserPromptBody,i as attentionNotification,l as createMainThreadTitleController,u as createWorkerTitleController,a as notificationBody,f as notificationExtension,o as promptTitle,d as sendSystemNotification,s as settledNotification,c as shellTabTitle,t as supportsShellTitle,n as warrantsAttentionNotification,r as warrantsSettledNotification};
@@ -0,0 +1,2 @@
1
+ function e({agentRunning:e,askUserBlocked:t}){return e&&!t}function t(e){return!e}function n({hasUI:e,mode:t}){return e&&t!==`rpc`}function r(e){return e[0]?.question??`The agent is waiting for your feedback.`}exports.askUserPromptBody=r,exports.supportsShellTitle=n,exports.warrantsAttentionNotification=e,exports.warrantsSettledNotification=t;
2
+ //# sourceMappingURL=notificationPolicy.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationPolicy.cjs","names":[],"sources":["../../src/services/notificationPolicy.ts"],"sourcesContent":["const DEFAULT_ASK_USER_BODY = 'The agent is waiting for your feedback.';\nconst RPC_MODE = 'rpc';\n\nexport interface AttentionState {\n agentRunning: boolean;\n askUserBlocked: boolean;\n}\n\nexport interface ShellSurface {\n hasUI: boolean;\n mode?: string;\n}\n\n/**\n * A dialog is worth interrupting the user for only while the agent holds the turn.\n *\n * Outside a run the user opened the dialog themselves and is already looking at\n * it. An ask-user prompt is announced by its own event, so the dialog it then\n * opens must stay silent or the same question notifies twice.\n */\nexport function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean {\n return agentRunning && !askUserBlocked;\n}\n\n/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */\nexport function warrantsSettledNotification(hasPendingMessages: boolean): boolean {\n return !hasPendingMessages;\n}\n\n/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */\nexport function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean {\n return hasUI && mode !== RPC_MODE;\n}\n\n/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */\nexport function askUserPromptBody(questions: readonly { question: string }[]): string {\n return questions[0]?.question ?? DEFAULT_ASK_USER_BODY;\n}\n"],"mappings":"AAoBA,SAAgB,EAA8B,CAAE,eAAc,kBAA2C,CACvG,OAAO,GAAgB,CAAC,CAC1B,CAGA,SAAgB,EAA4B,EAAsC,CAChF,MAAO,CAAC,CACV,CAGA,SAAgB,EAAmB,CAAE,QAAO,QAA+B,CACzE,OAAO,GAAS,IAAS,KAC3B,CAGA,SAAgB,EAAkB,EAAoD,CACpF,OAAO,EAAU,EAAE,EAAE,UAAY,yCACnC"}
@@ -0,0 +1,28 @@
1
+ //#region src/services/notificationPolicy.d.ts
2
+ interface AttentionState {
3
+ agentRunning: boolean;
4
+ askUserBlocked: boolean;
5
+ }
6
+ interface ShellSurface {
7
+ hasUI: boolean;
8
+ mode?: string;
9
+ }
10
+ /**
11
+ * A dialog is worth interrupting the user for only while the agent holds the turn.
12
+ *
13
+ * Outside a run the user opened the dialog themselves and is already looking at
14
+ * it. An ask-user prompt is announced by its own event, so the dialog it then
15
+ * opens must stay silent or the same question notifies twice.
16
+ */
17
+ declare function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean;
18
+ /** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */
19
+ declare function warrantsSettledNotification(hasPendingMessages: boolean): boolean;
20
+ /** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */
21
+ declare function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean;
22
+ /** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */
23
+ declare function askUserPromptBody(questions: readonly {
24
+ question: string;
25
+ }[]): string;
26
+ //#endregion
27
+ export { AttentionState, ShellSurface, askUserPromptBody, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
28
+ //# sourceMappingURL=notificationPolicy.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationPolicy.d.cts","names":[],"sources":["../../src/services/notificationPolicy.ts"],"mappings":";UAGiB;EACf;EACA;;UAGe;EACf;EACA;;;;;;;;;iBAUc,gCAAgC,cAAc,kBAAkB;;iBAKhE,4BAA4B;;iBAK5B,qBAAqB,OAAO,QAAQ;;iBAKpC,kBAAkB;EAAsB"}
@@ -0,0 +1,28 @@
1
+ //#region src/services/notificationPolicy.d.ts
2
+ interface AttentionState {
3
+ agentRunning: boolean;
4
+ askUserBlocked: boolean;
5
+ }
6
+ interface ShellSurface {
7
+ hasUI: boolean;
8
+ mode?: string;
9
+ }
10
+ /**
11
+ * A dialog is worth interrupting the user for only while the agent holds the turn.
12
+ *
13
+ * Outside a run the user opened the dialog themselves and is already looking at
14
+ * it. An ask-user prompt is announced by its own event, so the dialog it then
15
+ * opens must stay silent or the same question notifies twice.
16
+ */
17
+ declare function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean;
18
+ /** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */
19
+ declare function warrantsSettledNotification(hasPendingMessages: boolean): boolean;
20
+ /** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */
21
+ declare function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean;
22
+ /** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */
23
+ declare function askUserPromptBody(questions: readonly {
24
+ question: string;
25
+ }[]): string;
26
+ //#endregion
27
+ export { AttentionState, ShellSurface, askUserPromptBody, supportsShellTitle, warrantsAttentionNotification, warrantsSettledNotification };
28
+ //# sourceMappingURL=notificationPolicy.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationPolicy.d.mts","names":[],"sources":["../../src/services/notificationPolicy.ts"],"mappings":";UAGiB;EACf;EACA;;UAGe;EACf;EACA;;;;;;;;;iBAUc,gCAAgC,cAAc,kBAAkB;;iBAKhE,4BAA4B;;iBAK5B,qBAAqB,OAAO,QAAQ;;iBAKpC,kBAAkB;EAAsB"}
@@ -0,0 +1,2 @@
1
+ function e({agentRunning:e,askUserBlocked:t}){return e&&!t}function t(e){return!e}function n({hasUI:e,mode:t}){return e&&t!==`rpc`}function r(e){return e[0]?.question??`The agent is waiting for your feedback.`}export{r as askUserPromptBody,n as supportsShellTitle,e as warrantsAttentionNotification,t as warrantsSettledNotification};
2
+ //# sourceMappingURL=notificationPolicy.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationPolicy.mjs","names":[],"sources":["../../src/services/notificationPolicy.ts"],"sourcesContent":["const DEFAULT_ASK_USER_BODY = 'The agent is waiting for your feedback.';\nconst RPC_MODE = 'rpc';\n\nexport interface AttentionState {\n agentRunning: boolean;\n askUserBlocked: boolean;\n}\n\nexport interface ShellSurface {\n hasUI: boolean;\n mode?: string;\n}\n\n/**\n * A dialog is worth interrupting the user for only while the agent holds the turn.\n *\n * Outside a run the user opened the dialog themselves and is already looking at\n * it. An ask-user prompt is announced by its own event, so the dialog it then\n * opens must stay silent or the same question notifies twice.\n */\nexport function warrantsAttentionNotification({ agentRunning, askUserBlocked }: AttentionState): boolean {\n return agentRunning && !askUserBlocked;\n}\n\n/** A queued follow-up means the agent keeps working, so the run has not stopped for the user. */\nexport function warrantsSettledNotification(hasPendingMessages: boolean): boolean {\n return !hasPendingMessages;\n}\n\n/** Terminal title escapes only mean something on an attached terminal; RPC output stays clean. */\nexport function supportsShellTitle({ hasUI, mode }: ShellSurface): boolean {\n return hasUI && mode !== RPC_MODE;\n}\n\n/** The question an ask-user prompt is blocking on, or a stand-in when it asked none. */\nexport function askUserPromptBody(questions: readonly { question: string }[]): string {\n return questions[0]?.question ?? DEFAULT_ASK_USER_BODY;\n}\n"],"mappings":"AAoBA,SAAgB,EAA8B,CAAE,eAAc,kBAA2C,CACvG,OAAO,GAAgB,CAAC,CAC1B,CAGA,SAAgB,EAA4B,EAAsC,CAChF,MAAO,CAAC,CACV,CAGA,SAAgB,EAAmB,CAAE,QAAO,QAA+B,CACzE,OAAO,GAAS,IAAS,KAC3B,CAGA,SAAgB,EAAkB,EAAoD,CACpF,OAAO,EAAU,EAAE,EAAE,UAAY,yCACnC"}
@@ -0,0 +1,2 @@
1
+ const e=require("../_virtual/_rolldown/runtime.cjs");let t=require("node:path");t=e.__toESM(t,1);function n(e){return e.replace(/\s+/g,` `).trim()}function r(e,t){return e.length>t?`${e.slice(0,t-1)}…`:e}function i(e){let t=n(e);return t?r(t,36):void 0}function a({cwd:e,sessionName:n,prompt:r}){let i=t.default.basename(e),a=n??r;return a?`π - ${a} - ${i}`:`π - ${i}`}function o(e){return r(n(e),240)}function s(e){return{title:`Pi needs your input`,subtitle:`Approval or feedback required`,body:e}}function c({cwd:e,sessionName:n}){return{title:`Pi finished`,subtitle:n??t.default.basename(e),body:`The agent finished its work and is waiting for you.`}}exports.attentionNotification=s,exports.notificationBody=o,exports.promptTitle=i,exports.settledNotification=c,exports.shellTabTitle=a;
2
+ //# sourceMappingURL=notificationText.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationText.cjs","names":["path"],"sources":["../../src/services/notificationText.ts"],"sourcesContent":["import path from 'node:path';\nimport type { DesktopNotification } from '../types/notifications.ts';\n\nconst ATTENTION_SUBTITLE = 'Approval or feedback required';\nconst ATTENTION_TITLE = 'Pi needs your input';\nconst ELLIPSIS = '…';\nconst MAX_BODY_LENGTH = 240;\nconst MAX_PROMPT_TITLE_LENGTH = 36;\nconst SETTLED_BODY = 'The agent finished its work and is waiting for you.';\nconst SETTLED_TITLE = 'Pi finished';\nconst SHELL_TITLE_SEPARATOR = ' - ';\nconst SHELL_TITLE_PREFIX = 'π';\n\nfunction collapseWhitespace(value: string): string {\n return value.replace(/\\s+/g, ' ').trim();\n}\n\nfunction truncate(value: string, maxLength: number): string {\n return value.length > maxLength ? `${value.slice(0, maxLength - 1)}${ELLIPSIS}` : value;\n}\n\n/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */\nexport function promptTitle(prompt: string): string | undefined {\n const normalized = collapseWhitespace(prompt);\n return normalized ? truncate(normalized, MAX_PROMPT_TITLE_LENGTH) : undefined;\n}\n\nexport interface ShellTabTitleInput {\n cwd: string;\n sessionName?: string;\n prompt?: string;\n}\n\n/**\n * The idle shell-tab title: `π - <label> - <repository>`.\n *\n * The session name wins over the first prompt because the user chose it. With\n * neither, the repository alone still tells two tabs apart.\n */\nexport function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string {\n const repository = path.basename(cwd);\n const label = sessionName ?? prompt;\n return label\n ? `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${label}${SHELL_TITLE_SEPARATOR}${repository}`\n : `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${repository}`;\n}\n\n/** Notification bodies are one line in a system panel, so they collapse and truncate. */\nexport function notificationBody(body: string): string {\n return truncate(collapseWhitespace(body), MAX_BODY_LENGTH);\n}\n\n/** Sent while the agent holds the turn and something is blocking on the user. */\nexport function attentionNotification(body: string): DesktopNotification {\n return { title: ATTENTION_TITLE, subtitle: ATTENTION_SUBTITLE, body };\n}\n\n/** Sent once a run has stopped, subtitled with whichever name the user recognises. */\nexport function settledNotification({ cwd, sessionName }: { cwd: string; sessionName?: string }): DesktopNotification {\n return { title: SETTLED_TITLE, subtitle: sessionName ?? path.basename(cwd), body: SETTLED_BODY };\n}\n"],"mappings":"iGAaA,SAAS,EAAmB,EAAuB,CACjD,OAAO,EAAM,QAAQ,OAAQ,GAAG,CAAC,CAAC,KAAK,CACzC,CAEA,SAAS,EAAS,EAAe,EAA2B,CAC1D,OAAO,EAAM,OAAS,EAAY,GAAG,EAAM,MAAM,EAAG,EAAY,CAAC,KAAiB,CACpF,CAGA,SAAgB,EAAY,EAAoC,CAC9D,IAAM,EAAa,EAAmB,CAAM,EAC5C,OAAO,EAAa,EAAS,EAAY,EAAuB,EAAI,IAAA,EACtE,CAcA,SAAgB,EAAc,CAAE,MAAK,cAAa,UAAsC,CACtF,IAAM,EAAaA,EAAAA,QAAK,SAAS,CAAG,EAC9B,EAAQ,GAAe,EAC7B,OAAO,EACH,OAAgD,OAAgC,IAChF,OAAgD,GACtD,CAGA,SAAgB,EAAiB,EAAsB,CACrD,OAAO,EAAS,EAAmB,CAAI,EAAG,GAAe,CAC3D,CAGA,SAAgB,EAAsB,EAAmC,CACvE,MAAO,CAAE,MAAO,sBAAiB,SAAU,gCAAoB,MAAK,CACtE,CAGA,SAAgB,EAAoB,CAAE,MAAK,eAA2E,CACpH,MAAO,CAAE,MAAO,cAAe,SAAU,GAAeA,EAAAA,QAAK,SAAS,CAAG,EAAG,KAAM,qDAAa,CACjG"}
@@ -0,0 +1,28 @@
1
+ import { DesktopNotification } from "../types/notifications.cjs";
2
+ //#region src/services/notificationText.d.ts
3
+ /** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */
4
+ declare function promptTitle(prompt: string): string | undefined;
5
+ interface ShellTabTitleInput {
6
+ cwd: string;
7
+ sessionName?: string;
8
+ prompt?: string;
9
+ }
10
+ /**
11
+ * The idle shell-tab title: `π - <label> - <repository>`.
12
+ *
13
+ * The session name wins over the first prompt because the user chose it. With
14
+ * neither, the repository alone still tells two tabs apart.
15
+ */
16
+ declare function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string;
17
+ /** Notification bodies are one line in a system panel, so they collapse and truncate. */
18
+ declare function notificationBody(body: string): string;
19
+ /** Sent while the agent holds the turn and something is blocking on the user. */
20
+ declare function attentionNotification(body: string): DesktopNotification;
21
+ /** Sent once a run has stopped, subtitled with whichever name the user recognises. */
22
+ declare function settledNotification({ cwd, sessionName }: {
23
+ cwd: string;
24
+ sessionName?: string;
25
+ }): DesktopNotification;
26
+ //#endregion
27
+ export { ShellTabTitleInput, attentionNotification, notificationBody, promptTitle, settledNotification, shellTabTitle };
28
+ //# sourceMappingURL=notificationText.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationText.d.cts","names":[],"sources":["../../src/services/notificationText.ts"],"mappings":";;;iBAsBgB,YAAY;UAKX;EACf;EACA;EACA;;;;;;;;iBASc,gBAAgB,KAAK,aAAa,UAAU;;iBAS5C,iBAAiB;;iBAKjB,sBAAsB,eAAe;;iBAKrC,sBAAsB,KAAK;EAAiB;EAAa;IAAyB"}
@@ -0,0 +1,28 @@
1
+ import { DesktopNotification } from "../types/notifications.mjs";
2
+ //#region src/services/notificationText.d.ts
3
+ /** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */
4
+ declare function promptTitle(prompt: string): string | undefined;
5
+ interface ShellTabTitleInput {
6
+ cwd: string;
7
+ sessionName?: string;
8
+ prompt?: string;
9
+ }
10
+ /**
11
+ * The idle shell-tab title: `π - <label> - <repository>`.
12
+ *
13
+ * The session name wins over the first prompt because the user chose it. With
14
+ * neither, the repository alone still tells two tabs apart.
15
+ */
16
+ declare function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string;
17
+ /** Notification bodies are one line in a system panel, so they collapse and truncate. */
18
+ declare function notificationBody(body: string): string;
19
+ /** Sent while the agent holds the turn and something is blocking on the user. */
20
+ declare function attentionNotification(body: string): DesktopNotification;
21
+ /** Sent once a run has stopped, subtitled with whichever name the user recognises. */
22
+ declare function settledNotification({ cwd, sessionName }: {
23
+ cwd: string;
24
+ sessionName?: string;
25
+ }): DesktopNotification;
26
+ //#endregion
27
+ export { ShellTabTitleInput, attentionNotification, notificationBody, promptTitle, settledNotification, shellTabTitle };
28
+ //# sourceMappingURL=notificationText.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationText.d.mts","names":[],"sources":["../../src/services/notificationText.ts"],"mappings":";;;iBAsBgB,YAAY;UAKX;EACf;EACA;EACA;;;;;;;;iBASc,gBAAgB,KAAK,aAAa,UAAU;;iBAS5C,iBAAiB;;iBAKjB,sBAAsB,eAAe;;iBAKrC,sBAAsB,KAAK;EAAiB;EAAa;IAAyB"}
@@ -0,0 +1,2 @@
1
+ import e from"node:path";function t(e){return e.replace(/\s+/g,` `).trim()}function n(e,t){return e.length>t?`${e.slice(0,t-1)}…`:e}function r(e){let r=t(e);return r?n(r,36):void 0}function i({cwd:t,sessionName:n,prompt:r}){let i=e.basename(t),a=n??r;return a?`π - ${a} - ${i}`:`π - ${i}`}function a(e){return n(t(e),240)}function o(e){return{title:`Pi needs your input`,subtitle:`Approval or feedback required`,body:e}}function s({cwd:t,sessionName:n}){return{title:`Pi finished`,subtitle:n??e.basename(t),body:`The agent finished its work and is waiting for you.`}}export{o as attentionNotification,a as notificationBody,r as promptTitle,s as settledNotification,i as shellTabTitle};
2
+ //# sourceMappingURL=notificationText.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notificationText.mjs","names":[],"sources":["../../src/services/notificationText.ts"],"sourcesContent":["import path from 'node:path';\nimport type { DesktopNotification } from '../types/notifications.ts';\n\nconst ATTENTION_SUBTITLE = 'Approval or feedback required';\nconst ATTENTION_TITLE = 'Pi needs your input';\nconst ELLIPSIS = '…';\nconst MAX_BODY_LENGTH = 240;\nconst MAX_PROMPT_TITLE_LENGTH = 36;\nconst SETTLED_BODY = 'The agent finished its work and is waiting for you.';\nconst SETTLED_TITLE = 'Pi finished';\nconst SHELL_TITLE_SEPARATOR = ' - ';\nconst SHELL_TITLE_PREFIX = 'π';\n\nfunction collapseWhitespace(value: string): string {\n return value.replace(/\\s+/g, ' ').trim();\n}\n\nfunction truncate(value: string, maxLength: number): string {\n return value.length > maxLength ? `${value.slice(0, maxLength - 1)}${ELLIPSIS}` : value;\n}\n\n/** The first user prompt, shortened to fit a shell tab. A blank prompt names nothing. */\nexport function promptTitle(prompt: string): string | undefined {\n const normalized = collapseWhitespace(prompt);\n return normalized ? truncate(normalized, MAX_PROMPT_TITLE_LENGTH) : undefined;\n}\n\nexport interface ShellTabTitleInput {\n cwd: string;\n sessionName?: string;\n prompt?: string;\n}\n\n/**\n * The idle shell-tab title: `π - <label> - <repository>`.\n *\n * The session name wins over the first prompt because the user chose it. With\n * neither, the repository alone still tells two tabs apart.\n */\nexport function shellTabTitle({ cwd, sessionName, prompt }: ShellTabTitleInput): string {\n const repository = path.basename(cwd);\n const label = sessionName ?? prompt;\n return label\n ? `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${label}${SHELL_TITLE_SEPARATOR}${repository}`\n : `${SHELL_TITLE_PREFIX}${SHELL_TITLE_SEPARATOR}${repository}`;\n}\n\n/** Notification bodies are one line in a system panel, so they collapse and truncate. */\nexport function notificationBody(body: string): string {\n return truncate(collapseWhitespace(body), MAX_BODY_LENGTH);\n}\n\n/** Sent while the agent holds the turn and something is blocking on the user. */\nexport function attentionNotification(body: string): DesktopNotification {\n return { title: ATTENTION_TITLE, subtitle: ATTENTION_SUBTITLE, body };\n}\n\n/** Sent once a run has stopped, subtitled with whichever name the user recognises. */\nexport function settledNotification({ cwd, sessionName }: { cwd: string; sessionName?: string }): DesktopNotification {\n return { title: SETTLED_TITLE, subtitle: sessionName ?? path.basename(cwd), body: SETTLED_BODY };\n}\n"],"mappings":"yBAaA,SAAS,EAAmB,EAAuB,CACjD,OAAO,EAAM,QAAQ,OAAQ,GAAG,CAAC,CAAC,KAAK,CACzC,CAEA,SAAS,EAAS,EAAe,EAA2B,CAC1D,OAAO,EAAM,OAAS,EAAY,GAAG,EAAM,MAAM,EAAG,EAAY,CAAC,KAAiB,CACpF,CAGA,SAAgB,EAAY,EAAoC,CAC9D,IAAM,EAAa,EAAmB,CAAM,EAC5C,OAAO,EAAa,EAAS,EAAY,EAAuB,EAAI,IAAA,EACtE,CAcA,SAAgB,EAAc,CAAE,MAAK,cAAa,UAAsC,CACtF,IAAM,EAAa,EAAK,SAAS,CAAG,EAC9B,EAAQ,GAAe,EAC7B,OAAO,EACH,OAAgD,OAAgC,IAChF,OAAgD,GACtD,CAGA,SAAgB,EAAiB,EAAsB,CACrD,OAAO,EAAS,EAAmB,CAAI,EAAG,GAAe,CAC3D,CAGA,SAAgB,EAAsB,EAAmC,CACvE,MAAO,CAAE,MAAO,sBAAiB,SAAU,gCAAoB,MAAK,CACtE,CAGA,SAAgB,EAAoB,CAAE,MAAK,eAA2E,CACpH,MAAO,CAAE,MAAO,cAAe,SAAU,GAAe,EAAK,SAAS,CAAG,EAAG,KAAM,qDAAa,CACjG"}
@@ -0,0 +1,33 @@
1
+ //#region src/types/notifications.d.ts
2
+ /** Writes a shell-tab title to whatever terminal surface the host owns. */
3
+ type WriteTitle = (title: string) => void;
4
+ /** The four things a title controller can be asked to do, and its method names. */
5
+ type ShellTitleAction = 'dispose' | 'set' | 'start' | 'stop';
6
+ /** One instruction for a title controller, and the message shape its worker consumes. */
7
+ interface ShellTitleCommand {
8
+ action: ShellTitleAction;
9
+ title: string;
10
+ }
11
+ /**
12
+ * Drives the shell tab title across a run.
13
+ *
14
+ * `start` animates a spinner in front of the title until `stop` restores the
15
+ * idle one; `set` writes a title without animating; `dispose` writes the final
16
+ * title and releases whatever machinery the implementation owns. Every method
17
+ * takes the writer so the controller never has to hold a session reference.
18
+ */
19
+ interface ShellTitleController {
20
+ set(title: string, write: WriteTitle): void;
21
+ start(title: string, write: WriteTitle): void;
22
+ stop(title: string, write: WriteTitle): void;
23
+ dispose(title: string, write: WriteTitle): void;
24
+ }
25
+ /** One OS-level desktop notification. */
26
+ interface DesktopNotification {
27
+ title: string;
28
+ subtitle: string;
29
+ body: string;
30
+ }
31
+ //#endregion
32
+ export { DesktopNotification, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle };
33
+ //# sourceMappingURL=notifications.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.cts","names":[],"sources":["../../src/types/notifications.ts"],"mappings":";;KACY,cAAc;;KAGd;;UAGK;EACf,QAAQ;EACR;;;;;;;;;;UAWe;EACf,IAAI,eAAe,OAAO;EAC1B,MAAM,eAAe,OAAO;EAC5B,KAAK,eAAe,OAAO;EAC3B,QAAQ,eAAe,OAAO;;;UAIf;EACf;EACA;EACA"}
@@ -0,0 +1,33 @@
1
+ //#region src/types/notifications.d.ts
2
+ /** Writes a shell-tab title to whatever terminal surface the host owns. */
3
+ type WriteTitle = (title: string) => void;
4
+ /** The four things a title controller can be asked to do, and its method names. */
5
+ type ShellTitleAction = 'dispose' | 'set' | 'start' | 'stop';
6
+ /** One instruction for a title controller, and the message shape its worker consumes. */
7
+ interface ShellTitleCommand {
8
+ action: ShellTitleAction;
9
+ title: string;
10
+ }
11
+ /**
12
+ * Drives the shell tab title across a run.
13
+ *
14
+ * `start` animates a spinner in front of the title until `stop` restores the
15
+ * idle one; `set` writes a title without animating; `dispose` writes the final
16
+ * title and releases whatever machinery the implementation owns. Every method
17
+ * takes the writer so the controller never has to hold a session reference.
18
+ */
19
+ interface ShellTitleController {
20
+ set(title: string, write: WriteTitle): void;
21
+ start(title: string, write: WriteTitle): void;
22
+ stop(title: string, write: WriteTitle): void;
23
+ dispose(title: string, write: WriteTitle): void;
24
+ }
25
+ /** One OS-level desktop notification. */
26
+ interface DesktopNotification {
27
+ title: string;
28
+ subtitle: string;
29
+ body: string;
30
+ }
31
+ //#endregion
32
+ export { DesktopNotification, ShellTitleAction, ShellTitleCommand, ShellTitleController, WriteTitle };
33
+ //# sourceMappingURL=notifications.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.mts","names":[],"sources":["../../src/types/notifications.ts"],"mappings":";;KACY,cAAc;;KAGd;;UAGK;EACf,QAAQ;EACR;;;;;;;;;;UAWe;EACf,IAAI,eAAe,OAAO;EAC1B,MAAM,eAAe,OAAO;EAC5B,KAAK,eAAe,OAAO;EAC3B,QAAQ,eAAe,OAAO;;;UAIf;EACf;EACA;EACA"}
package/llms.txt ADDED
@@ -0,0 +1,7 @@
1
+ # Doom Pi Notification
2
+
3
+ Desktop notifications and an animated shell-tab title for Doom Pi sessions.
4
+
5
+ ## Resources
6
+
7
+ - [Package README](./README.md): what is announced and when, the worker-thread shell-title animator, and the rules that keep a session quiet.
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@agimon-ai/doompi-notification",
3
+ "version": "0.0.1-alpha.2",
4
+ "description": "Desktop notifications and an animated shell-tab title for DoomPi sessions.",
5
+ "keywords": [
6
+ "ai",
7
+ "coding-agent",
8
+ "desktop-notifications",
9
+ "developer-tools",
10
+ "doompi",
11
+ "notifications",
12
+ "pi-coding-agent",
13
+ "pi-extension",
14
+ "pi-package",
15
+ "shell-title",
16
+ "terminal-title"
17
+ ],
18
+ "homepage": "https://agimon.ai",
19
+ "license": "MIT",
20
+ "author": "Vuong Ngo",
21
+ "files": [
22
+ "dist",
23
+ "llms.txt",
24
+ "README.md",
25
+ "LICENSE",
26
+ "package.json"
27
+ ],
28
+ "type": "module",
29
+ "main": "./dist/index.cjs",
30
+ "types": "./dist/index.d.mts",
31
+ "jsnext:main": "./dist/index.mjs",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.mts",
35
+ "import": "./dist/index.mjs",
36
+ "require": "./dist/index.cjs"
37
+ },
38
+ "./extensions/pi": {
39
+ "types": "./dist/extensions/pi.d.mts",
40
+ "import": "./dist/extensions/pi.mjs",
41
+ "require": "./dist/extensions/pi.cjs"
42
+ },
43
+ "./package.json": "./package.json"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@deepseek-ai/cordis": "4.0.1",
50
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.24"
51
+ },
52
+ "devDependencies": {
53
+ "@earendil-works/pi-coding-agent": "0.84.2",
54
+ "@types/node": "26.1.1",
55
+ "@vitest/coverage-v8": "4.1.10",
56
+ "tsdown": "0.22.14",
57
+ "typescript": "6.0.3",
58
+ "vitest": "4.1.10",
59
+ "@agimon-ai/vibe-lint-plugin-doom-extension": "0.0.1-alpha.24"
60
+ },
61
+ "peerDependencies": {
62
+ "@earendil-works/pi-coding-agent": "0.84.2"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "@earendil-works/pi-coding-agent": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "engines": {
70
+ "node": ">=22.19.0"
71
+ },
72
+ "pi": {
73
+ "extensions": [
74
+ "./dist/extensions/pi.mjs"
75
+ ]
76
+ },
77
+ "scripts": {
78
+ "build": "tsdown",
79
+ "test": "vitest --run",
80
+ "typecheck": "tsc --noEmit",
81
+ "lint": "oxlint . && oxfmt . --check",
82
+ "fixcode": "oxlint . --fix && oxfmt ."
83
+ }
84
+ }