@kingcrab/pi-imessage 0.0.42 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +101 -108
  2. package/dist/agent/behavior.js +79 -0
  3. package/dist/agent/chats.js +67 -0
  4. package/dist/agent/commands.js +163 -0
  5. package/dist/agent/compaction.js +51 -0
  6. package/dist/agent/deliver.js +139 -0
  7. package/dist/agent/direct-send.js +68 -0
  8. package/dist/agent/harness.js +79 -0
  9. package/dist/agent/health.js +32 -0
  10. package/dist/agent/models.js +69 -0
  11. package/dist/agent/progress-format.js +87 -0
  12. package/dist/agent/progress.js +135 -0
  13. package/dist/agent/prompt.js +79 -0
  14. package/dist/agent/replies.js +9 -0
  15. package/dist/agent/run.js +159 -0
  16. package/dist/agent/scheduling.js +290 -0
  17. package/dist/agent/time.js +18 -0
  18. package/dist/cli.js +88 -191
  19. package/dist/config/settings.js +60 -0
  20. package/dist/extensions/browser.js +326 -0
  21. package/dist/extensions/final-text.js +23 -0
  22. package/dist/extensions/memory.js +109 -0
  23. package/dist/extensions/message.js +33 -0
  24. package/dist/extensions/read-image.js +72 -0
  25. package/dist/extensions/schedules.js +41 -0
  26. package/dist/extensions/subagent.js +54 -0
  27. package/dist/extensions/web.js +99 -0
  28. package/dist/extensions/workspace.js +98 -0
  29. package/dist/main.js +571 -74
  30. package/dist/transport/attachments.js +37 -0
  31. package/dist/transport/http.js +110 -0
  32. package/dist/{rich-text.js → transport/rich-text.js} +25 -13
  33. package/dist/{send.js → transport/send.js} +13 -13
  34. package/dist/transport/watch.js +124 -0
  35. package/dist/web/app.js +748 -0
  36. package/dist/web/history.js +87 -0
  37. package/dist/web/memory.js +81 -0
  38. package/dist/web/page.js +28 -0
  39. package/dist/web/schedules.js +74 -0
  40. package/dist/web/server.js +348 -0
  41. package/dist/web/style.css +543 -0
  42. package/dist/web/tasks.js +63 -0
  43. package/docs/adr/0001-durable-owns-state.md +11 -0
  44. package/docs/adr/0002-module-layering.md +11 -0
  45. package/docs/adr/0003-replies-at-most-once.md +11 -0
  46. package/docs/adr/0004-native-extensions.md +11 -0
  47. package/docs/adr/0005-history-migration.md +11 -0
  48. package/docs/adr/0006-jsonl-storage.md +11 -0
  49. package/docs/adr/0007-images-by-path.md +11 -0
  50. package/docs/adr/0008-compaction-and-run.md +11 -0
  51. package/docs/adr/0009-one-shot-cutover.md +11 -0
  52. package/docs/adr/0010-test-budget.md +11 -0
  53. package/docs/adr/0011-reconcile-unsent-answers.md +24 -0
  54. package/docs/adr/0012-run-in-owned-conversation.md +22 -0
  55. package/docs/adr/0013-thin-adapters-around-durable.md +18 -0
  56. package/docs/adr/0014-ephemeral-private-storage.md +13 -0
  57. package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +21 -0
  58. package/docs/adr/0016-process-http-transport.md +21 -0
  59. package/docs/adr/0017-browser-cli-owned-scope.md +22 -0
  60. package/docs/adr/0018-tasks-without-prompt-api.md +19 -0
  61. package/docs/adr/0019-remove-scheduled-services.md +41 -0
  62. package/docs/adr/0020-chat-task-progress.md +41 -0
  63. package/docs/adr/0021-readable-progress-without-link-previews.md +33 -0
  64. package/docs/adr/0022-progress-admission-and-settlement.md +35 -0
  65. package/docs/adr/0023-retain-progress-type-prefixes.md +18 -0
  66. package/docs/adr/0024-status-marker-before-type-prefix.md +13 -0
  67. package/docs/adr/0025-reusable-progress-and-command-preview.md +49 -0
  68. package/docs/adr/0026-inline-command-only-requests.md +16 -0
  69. package/docs/adr/0027-command-preview-width.md +10 -0
  70. package/docs/adr/0028-command-preview-120.md +8 -0
  71. package/docs/adr/0030-shared-web-read-cache.md +33 -0
  72. package/docs/adr/0031-quiet-compaction-progress.md +32 -0
  73. package/docs/adr/0032-native-background-schedules.md +58 -0
  74. package/docs/adr/0033-operator-scheduled-run.md +23 -0
  75. package/docs/adr/0034-machine-local-schedule-time.md +36 -0
  76. package/docs/adr/0035-external-business-task-extensions.md +56 -0
  77. package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +37 -0
  78. package/docs/adr/0037-restart-during-in-flight-work.md +30 -0
  79. package/docs/adr/0038-restart-any-time.md +25 -0
  80. package/docs/adr/0039-rerun-interrupted-tools.md +24 -0
  81. package/docs/adr/0040-chat-progress-notices.md +44 -0
  82. package/docs/adr/0041-restart-and-recovery.md +28 -0
  83. package/docs/adr/0042-background-schedules-and-workspace-extensions.md +44 -0
  84. package/docs/adr/0043-remove-legacy-migration.md +23 -0
  85. package/docs/adr/0044-message-channels.md +32 -0
  86. package/docs/adr/0045-chat-scheduling-tools.md +51 -0
  87. package/docs/adr/0046-selective-command-redaction.md +31 -0
  88. package/docs/adr/0047-one-scheduling-path.md +158 -0
  89. package/docs/adr/README.md +50 -0
  90. package/docs/api.md +92 -0
  91. package/docs/architecture.md +164 -0
  92. package/docs/browser.md +93 -0
  93. package/docs/memory.md +27 -0
  94. package/docs/screenshot.png +0 -0
  95. package/docs/workspace-extensions.md +133 -0
  96. package/examples/workspace-extensions/system-context/index.ts +69 -0
  97. package/examples/workspace-extensions/system-context/package.json +3 -0
  98. package/ops/README.md +267 -0
  99. package/ops/preflight.mjs +28 -0
  100. package/package.json +33 -45
  101. package/dist/agent.d.ts +0 -60
  102. package/dist/agent.d.ts.map +0 -1
  103. package/dist/agent.js +0 -715
  104. package/dist/agent.js.map +0 -1
  105. package/dist/cli.d.ts +0 -15
  106. package/dist/cli.d.ts.map +0 -1
  107. package/dist/cli.js.map +0 -1
  108. package/dist/imessage.d.ts +0 -33
  109. package/dist/imessage.d.ts.map +0 -1
  110. package/dist/imessage.js +0 -76
  111. package/dist/imessage.js.map +0 -1
  112. package/dist/logger.d.ts +0 -33
  113. package/dist/logger.d.ts.map +0 -1
  114. package/dist/logger.js +0 -104
  115. package/dist/logger.js.map +0 -1
  116. package/dist/main.d.ts +0 -5
  117. package/dist/main.d.ts.map +0 -1
  118. package/dist/main.js.map +0 -1
  119. package/dist/memory.d.ts +0 -53
  120. package/dist/memory.d.ts.map +0 -1
  121. package/dist/memory.js +0 -196
  122. package/dist/memory.js.map +0 -1
  123. package/dist/model-health.d.ts +0 -11
  124. package/dist/model-health.d.ts.map +0 -1
  125. package/dist/model-health.js +0 -67
  126. package/dist/model-health.js.map +0 -1
  127. package/dist/pipeline.d.ts +0 -27
  128. package/dist/pipeline.d.ts.map +0 -1
  129. package/dist/pipeline.js +0 -59
  130. package/dist/pipeline.js.map +0 -1
  131. package/dist/queue.d.ts +0 -23
  132. package/dist/queue.d.ts.map +0 -1
  133. package/dist/queue.js +0 -89
  134. package/dist/queue.js.map +0 -1
  135. package/dist/rich-text.d.ts +0 -26
  136. package/dist/rich-text.d.ts.map +0 -1
  137. package/dist/rich-text.js.map +0 -1
  138. package/dist/self-echo.d.ts +0 -19
  139. package/dist/self-echo.d.ts.map +0 -1
  140. package/dist/self-echo.js +0 -54
  141. package/dist/self-echo.js.map +0 -1
  142. package/dist/send.d.ts +0 -24
  143. package/dist/send.d.ts.map +0 -1
  144. package/dist/send.js.map +0 -1
  145. package/dist/settings.d.ts +0 -44
  146. package/dist/settings.d.ts.map +0 -1
  147. package/dist/settings.js +0 -80
  148. package/dist/settings.js.map +0 -1
  149. package/dist/store.d.ts +0 -57
  150. package/dist/store.d.ts.map +0 -1
  151. package/dist/store.js +0 -124
  152. package/dist/store.js.map +0 -1
  153. package/dist/tasks.d.ts +0 -95
  154. package/dist/tasks.d.ts.map +0 -1
  155. package/dist/tasks.js +0 -384
  156. package/dist/tasks.js.map +0 -1
  157. package/dist/types.d.ts +0 -93
  158. package/dist/types.d.ts.map +0 -1
  159. package/dist/types.js +0 -38
  160. package/dist/types.js.map +0 -1
  161. package/dist/watch.d.ts +0 -21
  162. package/dist/watch.d.ts.map +0 -1
  163. package/dist/watch.js +0 -219
  164. package/dist/watch.js.map +0 -1
  165. package/dist/web/data.d.ts +0 -11
  166. package/dist/web/data.d.ts.map +0 -1
  167. package/dist/web/data.js +0 -56
  168. package/dist/web/data.js.map +0 -1
  169. package/dist/web/html.d.ts +0 -5
  170. package/dist/web/html.d.ts.map +0 -1
  171. package/dist/web/html.js +0 -16
  172. package/dist/web/html.js.map +0 -1
  173. package/dist/web/index.d.ts +0 -23
  174. package/dist/web/index.d.ts.map +0 -1
  175. package/dist/web/index.js +0 -279
  176. package/dist/web/index.js.map +0 -1
  177. package/dist/web/render.d.ts +0 -11
  178. package/dist/web/render.d.ts.map +0 -1
  179. package/dist/web/render.js +0 -56
  180. package/dist/web/render.js.map +0 -1
  181. package/dist/web/templates/logs.eta +0 -75
  182. package/dist/web/templates/memory.eta +0 -91
  183. package/dist/web/templates/page.eta +0 -98
@@ -0,0 +1,133 @@
1
+ # Workspace extensions
2
+
3
+ Memory, web tools, chat routing and delivery remain built-in capabilities.
4
+ Business code belongs in `WORKING_DIR/extensions/<name>/index.ts` with its own
5
+ `config.json`. Both the operator and chat assistant can create it. This is trusted
6
+ host code, not a sandbox or an SDK `ExtensionAPI` adapter. Back up the extension
7
+ directories with their configuration. The
8
+ [system-context](../examples/workspace-extensions/system-context/) example loads
9
+ an optional workspace summary into the prompt (ADR 0036).
10
+
11
+ Tools without `replay` rerun after an interruption (ADR 0039). Declare
12
+ `replay: "unsafe"` for an effect that must not repeat.
13
+
14
+ ## Factory and reload
15
+
16
+ Use a CommonJS factory (`module.exports`) and CommonJS local dependencies
17
+ (`require`); put `{"type":"commonjs"}` in the extension's `package.json`.
18
+ Erasable TypeScript and type-only imports are supported. Runtime ESM modules
19
+ cannot be cache-evicted safely and are not part of this reload contract.
20
+ No dependencies are installed by the loader.
21
+
22
+ ```ts
23
+ module.exports = ({ config, defineExtension, defineTask }) => {
24
+ const Tick = defineTask({
25
+ name: "example.tick",
26
+ version: 1,
27
+ initial: () => ({ phase: "work" }),
28
+ phases: {
29
+ async work(task, runtime, context) {
30
+ await runtime.commit(() => ({
31
+ status: "terminal",
32
+ outcome: { status: "completed", result: {
33
+ summary: config.summary,
34
+ finishedAt: runtime.now()
35
+ } }
36
+ }), context);
37
+ }
38
+ },
39
+ async abort(task, runtime, context) {
40
+ await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), context);
41
+ }
42
+ });
43
+ return {
44
+ ...defineExtension({ name: "example", tasks: [Tick] }),
45
+ schedules: [{ id: "example-tick", name: "Example tick", enabled: true,
46
+ intervalMs: 3600000, task: Tick.definition.name }]
47
+ };
48
+ };
49
+ ```
50
+
51
+ The factory receives `workingDir`, parsed `config`, `Type`, `defineExtension`,
52
+ `defineTool`, `defineDoc`, `defineTask`, `configure`, `section`, and the generic
53
+ `ScheduledOutbox` document token. All primitives are the host's pinned versions.
54
+ Return a native extension with optional `schedules`; tools and multiple tasks
55
+ work without any schedules. Startup and `/reload` scan direct, non-hidden child
56
+ directories only, never skills or attachments. `reload_extensions` lets the chat
57
+ assistant apply its changes through the existing service owner, without sending
58
+ an iMessage or opening a second Harness.
59
+
60
+ Extension names, task names and schedule IDs must be unique. Schedule IDs are
61
+ stable state keys, not display names. Failed validation or initialization keeps
62
+ old definitions and schedule configuration. Reload refreshes CommonJS entry and
63
+ local dependency code plus configuration. Running invocations keep their code;
64
+ subsequent invocations use the replacement. Increment task versions and supply
65
+ native `migrate` handlers for changed saved inputs/checkpoints.
66
+
67
+ ## Scheduling and delivery
68
+
69
+ A schedule requires `id`, `name`, `enabled`, and a `task` name belonging to its
70
+ extension. Choose exactly one cadence: `time: "HH:MM"` or positive `intervalMs`.
71
+ Optional fields: `kind` (display metadata), `chatGuid` and JSON `input`.
72
+ An optional `initialize(tx, conversationId)` imports business state within the
73
+ host's atomic schedule commit. It must be replay-safe and must not send messages
74
+ or perform external writes; use native tasks for external actions.
75
+
76
+ The framework creates a dedicated conversation and background scheduler for each
77
+ schedule. Each occurrence delegates to the business task with:
78
+
79
+ - `jobId`, `date` (machine-local), `dueAt`, `startedAt`;
80
+ - `chatGuid` or null, and `config` containing the schedule's JSON `input` or null.
81
+
82
+ Successful business tasks return `{ summary, finishedAt, requestId? }`.
83
+ The wrapper's native receipt supplies read-only `/scheduled` history.
84
+ Daily schedules process only today's slot, without missed-date backfill;
85
+ interval schedules skip missed slots. Automatic and manual occurrences do not
86
+ overlap. Reload never resets a saved absolute deadline: new cadence applies
87
+ when computing the following slot. An explicitly aborted scheduler is not
88
+ silently recreated.
89
+
90
+ Commit business changes and `ScheduledOutbox.items` together, using a stable
91
+ `requestId`. The host reuses its direct-send receipts and passive chat records.
92
+ The destination must be reply-enabled. `sending`/`unknown` effects are never
93
+ replayed automatically; a queued item is not proof of delivery. Keep credentials
94
+ out of `settings.json`; the schedule view does not show extension configuration.
95
+
96
+ ## One-off runs through the same scheduler
97
+
98
+ For an existing factory that does not declare `runRequests`, the loader takes
99
+ that field directly from `config.json`; business code need not change. A factory
100
+ can instead explicitly supply its own `runRequests` declarations alongside its
101
+ `schedules`. For example, the extension's `config.json` can include:
102
+
103
+ ```json
104
+ {
105
+ "runRequests": [
106
+ { "id": "example-trial-1", "scheduleId": "example-tick", "at": "now" }
107
+ ]
108
+ }
109
+ ```
110
+
111
+ Apply the configuration with `reload_extensions` (or the existing `/reload`).
112
+ The target must be an enabled schedule belonging to that extension. `at` is
113
+ `"now"` or an absolute epoch-millisecond timestamp. There is no configuration
114
+ watcher or separate CLI/HTTP execution entry point.
115
+
116
+ The owner persists a background `imessage.schedule` task for the request, then
117
+ uses the same occurrence wrapper and business conversation as recurring runs.
118
+ A busy target delays the one-off until its active occurrence settles. The request
119
+ ID and resolved deadline survive reload/restart, including after completion.
120
+ Reuse the same declaration to recover it; use a new ID for a new intended run.
121
+ Conflicting reuse fails rather than changing an existing request.
122
+
123
+ Removing a request prevents future admission, not an already admitted business
124
+ task. Re-adding a consumed ID never runs it again. Business per-date dedupe and
125
+ delivery receipts still apply; a trial does not force another English card.
126
+ Regular task IDs and deadlines do not move. Historical operator runs remain
127
+ stored, but startup `--run-scheduled` flags are no longer accepted.
128
+
129
+ Removing an extension disables future schedule admission and removes its tools
130
+ and prompt sections, not current calls or historical records. Removed task code
131
+ is conservatively retained in memory until restart, because an old running call
132
+ can still admit children. Restart fails explicitly if unfinished work needs
133
+ missing task code or a missing schedule extension.
@@ -0,0 +1,69 @@
1
+ import type * as FileSystem from "node:fs/promises";
2
+ import type * as Paths from "node:path";
3
+ import type { defineExtension as DefineExtension, section as Section } from "@earendil-works/pi-durable";
4
+
5
+ const { constants, open } = require("node:fs/promises") as typeof FileSystem;
6
+ const { resolve } = require("node:path") as typeof Paths;
7
+ /** Per-file byte budget for System summary; dated history is never loaded here. */
8
+
9
+ async function readSummary(path: string, maxBytes: number, boundary: string) {
10
+ let file: Awaited<ReturnType<typeof open>> | undefined;
11
+ try {
12
+ file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
13
+ if (!(await file.stat()).isFile())
14
+ return "[System summary unavailable: expected a regular summary file.]";
15
+ const buffer = Buffer.alloc(maxBytes + 1);
16
+ let bytesRead = 0;
17
+ while (bytesRead < buffer.length) {
18
+ const result = await file.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
19
+ if (!result.bytesRead) break;
20
+ bytesRead += result.bytesRead;
21
+ }
22
+ const truncated = bytesRead > maxBytes;
23
+ // A streaming decode drops only an incomplete trailing UTF-8 character at the byte boundary.
24
+ const text = new TextDecoder("utf-8", { fatal: true })
25
+ .decode(buffer.subarray(0, Math.min(bytesRead, maxBytes)), { stream: truncated })
26
+ .trim();
27
+ const end = boundary ? text.indexOf(boundary) : -1;
28
+ if (end >= 0) return text.slice(0, end).trim();
29
+ return truncated
30
+ ? `${text}\n\n[System summary exceeded the ${maxBytes}-byte summary limit; the rest was not loaded. Read the full file on demand.]`
31
+ : text;
32
+ } catch (error) {
33
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return "";
34
+ return "[System summary summary unavailable.]";
35
+ } finally {
36
+ await file?.close();
37
+ }
38
+ }
39
+
40
+ module.exports = async ({
41
+ workingDir,
42
+ config,
43
+ defineExtension,
44
+ section,
45
+ }: {
46
+ workingDir: string;
47
+ config: Record<string, unknown>;
48
+ defineExtension: typeof DefineExtension;
49
+ section: typeof Section;
50
+ }) => {
51
+ if (
52
+ typeof config.summaryFile !== "string" ||
53
+ !config.summaryFile ||
54
+ !Number.isSafeInteger(config.maxBytes) ||
55
+ (config.maxBytes as number) <= 0 ||
56
+ (config.maxBytes as number) > 65536 ||
57
+ typeof config.boundary !== "string"
58
+ )
59
+ throw new Error("System context requires summaryFile, maxBytes (1..65536) and boundary");
60
+ const summary = await readSummary(
61
+ resolve(workingDir, config.summaryFile),
62
+ config.maxBytes as number,
63
+ config.boundary,
64
+ );
65
+ return defineExtension({
66
+ name: "system-context",
67
+ sections: [section("system_configuration", () => summary || undefined)],
68
+ });
69
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
package/ops/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # Operations: installation, deployment and restart
2
+
3
+ The owner has authorized deployment after each completed change, including
4
+ verification, plus committing and pushing each completed intended project change
5
+ after the required checks. Do not ask again for routine deployment or commit/push
6
+ approval in that scope. Stage explicit intended paths; exclude unrelated drafts,
7
+ temporary files and credentials. Verify the pushed remote revision; stop on a
8
+ rejected push, never force push. This does not authorize paid probes, test messages,
9
+ data cleanup or unrelated service changes. Stop for a new material safety or
10
+ data-integrity risk.
11
+ Never run old/new pipelines concurrently or use production data in tests.
12
+ Implementation readiness and open decisions are tracked in GitHub #33.
13
+
14
+ For ordinary updates, use [Routine deployment and restart](#routine-deployment-and-restart).
15
+ The one-time legacy migration is archived at tag `archive/legacy-migration` (ADR 0043).
16
+
17
+ ## Prerequisites
18
+
19
+ - macOS, Node >=22.22.2, Python 3 and authenticated Pi models.
20
+ - Terminal/Node Full Disk Access for Messages' SQLite database; Automation and
21
+ Accessibility permissions for Messages.app/rich-text sends; `/usr/bin/sips`.
22
+ - The existing memory CLI at `WORKING_DIR/skills/file-memory/memory_cli.py`.
23
+ - The installed `pi-browser` CLI and its pinned Playwright runtime. Run
24
+ `pi-browser setup` when needed and authorized; preflight never installs it. Preflight
25
+ resolves the wrapper in Pi's agent `bin/`, or a preflight-only
26
+ `PI_BROWSER_CLI_PATH` override, and runs `--version` with temporary browser state
27
+ that is removed afterward. A wrapper whose `doctor` reports MISSING is not ready.
28
+ Version readiness does not prove browser privacy, login or per-chat isolation.
29
+ - Reviewed native SQLite and image dependencies. Install dependencies with
30
+ `npm ci --ignore-scripts`; if native binaries are missing, review and approve
31
+ their build/download separately rather than silently enabling install scripts.
32
+
33
+ Safe local checks (no live models/messages/workspace):
34
+
35
+ ```sh
36
+ npm run check
37
+ npm test
38
+ node --experimental-strip-types ops/preflight.mjs
39
+ node --experimental-strip-types src/cli.ts --help
40
+ ```
41
+
42
+ For changes to the built-in browser, also run
43
+ `node --experimental-strip-types ops/browser-smoke.mjs`. This uses real headless
44
+ Chrome and the installed pinned CLI with temporary HOME/profiles, loopback pages
45
+ and synthetic login state; it never opens a personal profile or sends a message.
46
+ See [browser isolation and retention](../docs/browser.md).
47
+
48
+ `WORKING_DIR`, `WEB_HOST`, `WEB_PORT`, `WEB_ENABLED` and `MESSAGES_DB_PATH`
49
+ are explicit host configuration. `npm start`, the
50
+ executable CLI and the generated launchd job enable Node's environment proxy
51
+ support. `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` (and their lowercase forms) and
52
+ `NODE_OPTIONS` are retained when generating the job; review them before install.
53
+ CLI execution loads the current directory's `.env` using Node's parser; existing
54
+ environment values take precedence and a missing file is allowed. Set
55
+ `DOTENV_CONFIG_PATH` for another file. Module imports and `--help` do not load it.
56
+ Other read errors stop startup without printing file contents. `.env` is trusted
57
+ operator configuration, not a message attachment. Keep it private and out of Git.
58
+ Installation pins its current directory as the job's working directory, so the
59
+ same configuration path is used at launch. Selected environment values in the
60
+ plist are a snapshot; regenerate a reviewed job to change those overrides.
61
+ The job also pins the selected Pi agent directory as an absolute
62
+ `PI_CODING_AGENT_DIR` (including the default). Relative paths and `~` are resolved
63
+ at installation, after `.env` loading, so daemon HOME/cwd changes do not select a
64
+ different auth.json, models.json, settings.json or skill root. This selects a
65
+ configuration location; it does not copy credentials or grant access to it.
66
+
67
+ No proxy credentials are printed by installation. Application startup configures
68
+ one process-owned Undici transport after `.env` loading and before model/auth
69
+ refresh. Global Pi `httpProxy` supplies HTTP/HTTPS defaults; explicit environment
70
+ values win, lowercase proxy/bypass values take precedence, and `NO_PROXY` is
71
+ honored. Project-local `httpProxy` is ignored. `/reload` applies changed settings
72
+ for subsequent requests while old clients drain active requests. Shutdown stops
73
+ messaging and Harness work before releasing owned HTTP resources. Standalone
74
+ callers with injected runtimes retain transport control; deliberate fetch
75
+ overrides and dispatchers owned by other callers are not replaced on cleanup.
76
+
77
+ Pi settings in the agent directory and `WORKING_DIR/.pi/settings.json` supply
78
+ request policy; `/reload` refreshes it. `retry.provider.timeoutMs` overrides the
79
+ provider request timeout, otherwise `httpIdleTimeoutMs` supplies that default
80
+ (300000 ms; 0 uses Pi's effectively unlimited value). This forwards a request
81
+ option to supporting providers. Separately, application HTTP transport uses
82
+ `httpIdleTimeoutMs` for native header/body inactivity timers; 0 disables those
83
+ timers. An explicit provider request timeout can still limit a request when
84
+ socket-idle timers are disabled. Provider retry limits/delay caps and `retry`
85
+ generation policy are executed by Pi/Durable; transport remains SSE. These do
86
+ not replace the old host activity timer, prompt maximum-duration or
87
+ compaction-deadline behavior.
88
+
89
+ Web defaults to localhost:7750 and has no authentication. Never expose it to
90
+ untrusted networks.
91
+
92
+ Lifecycle logs use ISO timestamps on stdout/stderr: application startup (PID,
93
+ Node version and workspace), agent readiness/reload, the actual Web bind address
94
+ or disabled state, messaging readiness (cursor and poll/compaction intervals),
95
+ shutdown signal, resource cleanup and failures. Readiness is logged only after
96
+ the corresponding initialization succeeds; it is not a model or delivery health
97
+ check. Idle polls do not produce log lines. These lifecycle entries do not include
98
+ message text, tool arguments or proxy/auth configuration.
99
+ The generated launchd job redirects both streams to `WORKING_DIR/service.log`.
100
+ Foreground runs use the terminal or the controller's selected redirection; the
101
+ application does not open a second log file.
102
+
103
+ ## npm publishing
104
+
105
+ The public package contains compiled JavaScript and Web assets, not a TypeScript
106
+ executable under `node_modules`. `npm pack` builds through `prepack`;
107
+ `npm run test:package` verifies the packed executable and inert installer in a
108
+ temporary HOME/workspace without models or Messages.
109
+
110
+ `.github/workflows/publish.yml` uses the existing npm trusted publisher with
111
+ GitHub OIDC, Node 24 and `id-token: write`; no local npm login or token is needed.
112
+ It runs the full checks, tests and package smoke before publishing with provenance.
113
+ Future releases push a matching `v<package-version>` tag. To publish an existing
114
+ version after fixing packaging, dispatch the workflow on main with that exact
115
+ version; never move an already published GitHub tag.
116
+
117
+ ```sh
118
+ gh workflow run publish.yml --ref main -f version=1.0.0
119
+ ```
120
+
121
+ An installed npm service runs `dist/cli.js`; source deployments continue to use
122
+ `src/cli.ts`. Package installation does not start or reload a service.
123
+
124
+ ## Service identity
125
+
126
+ New `install` output uses `org.pi-imessage.service` and
127
+ `org.pi-imessage.service.plist`. For an installed
128
+ `me.changchen.pi-imessage-durable` job, privately preserve its plist/environment,
129
+ gracefully stop and unload that exact controller, verify PID/lock/port release,
130
+ then write and load the reviewed neutral job. Do not leave the old KeepAlive
131
+ controller loaded or start a Terminal copy alongside launchd. A foreground
132
+ Terminal service with no old-label job needs no controller change. Never rename
133
+ unrelated legacy jobs as part of this migration.
134
+
135
+ ## Routine deployment and restart
136
+
137
+ ### Choose the smallest operation
138
+
139
+ | Change | Action |
140
+ |---|---|
141
+ | Server, agent, transport or imported TypeScript | Restart from a new release directory; verify the change. |
142
+ | Web `app.js` / `style.css` only | Verify the served assets; no restart (the file cache checks metadata). |
143
+ | Documentation only | Commit and push; no reload or restart. |
144
+ | Configuration supported by `/reload` | Reload; restart only for startup-only configuration. Never send an iMessage to trigger it. |
145
+
146
+ Deploying means the change is live and verified, then committed and pushed.
147
+
148
+ ### Release directories
149
+
150
+ The service runs a fixed commit, never the main checkout:
151
+
152
+ ```sh
153
+ SHA=$(git -C /Users/clawbot/pi-imessage-next rev-parse origin/main)
154
+ RELEASE=/Users/clawbot/pi-imessage-releases/$SHA
155
+ git -C /Users/clawbot/pi-imessage-next worktree add --detach "$RELEASE" "$SHA"
156
+ (cd "$RELEASE" && npm ci --ignore-scripts && node --experimental-strip-types ops/preflight.mjs)
157
+ ```
158
+
159
+ Never edit a release directory; rollback is a routine restart from the previous
160
+ one. Keep the latest three; remove older ones with `git worktree remove` (no
161
+ `--force`) when no process or plist uses them. A release directory has no `.env`:
162
+ set `DOTENV_CONFIG_PATH` to the main checkout's `.env`.
163
+
164
+ ### 1. Identify the running service
165
+
166
+ ```sh
167
+ launchctl list | grep -Ei 'pi-imessage|pi-web' || true
168
+ lsof -nP -iTCP:7750 -sTCP:LISTEN # take PID from here, never from notes
169
+ ps -p "$PID" -o pid,ppid,lstart,command
170
+ lsof -a -p "$PID" -d cwd,1,2 -Fn
171
+ ```
172
+
173
+ The claw baseline is a **Terminal foreground process** (not launchd) running from
174
+ a release directory, with `WORKING_DIR=~/.pi/imessage-next`,
175
+ `PI_CODING_AGENT_DIR=~/.pi/agent`, Node 22, logs in `WORKING_DIR/service.log`, and
176
+ the owner-selected unauthenticated bind `0.0.0.0:7750` (trusted networks only).
177
+ A similarly named Pi web job does not control this service. Do not switch
178
+ controllers, install launchd or upgrade Node during a routine restart.
179
+
180
+ ### 2. Record the baseline (ADR 0038)
181
+
182
+ Restart at any time; no running work defers it. Before stopping, record the
183
+ counts of `sending` / `unknown` receipts from `/chat/data` (counts only, no chat
184
+ text). Never reset cursors, delete state or replay uncertain sends. Do not use
185
+ `/health/model`: it is a paid request.
186
+
187
+ ### 3. Run one private controller
188
+
189
+ In a `mktemp -d /tmp/pi-imessage-restart.XXXXXX` directory with `umask 077`:
190
+
191
+ - `environment.sh`: shell-quoted exports of the running process's `WORKING_DIR`,
192
+ `PI_CODING_AGENT_DIR`, `DOTENV_CONFIG_PATH`, Web/DB overrides, proxy variables
193
+ (both cases), `NODE_OPTIONS`, search keys, HOME, locale, `TZ` and PATH (keep
194
+ `~/.pi/agent/bin` and the Node directory). Write it directly; never print
195
+ values, `ps eww` output or credentials.
196
+ - `control.sh`: shell-quoted `OLD_PID`, `OLD_STARTED` (exact `ps -o lstart=`),
197
+ `EXPECTED_COMMAND`, `REPO` (the release directory), `NODE`, `WORKING_DIR`,
198
+ `WEB_PORT` and `LOG_FILE`.
199
+ - `restart.command`:
200
+
201
+ ```sh
202
+ #!/bin/bash
203
+ set -euo pipefail
204
+ umask 077
205
+ DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)"
206
+ source "$DEPLOY_DIR/environment.sh"
207
+ source "$DEPLOY_DIR/control.sh"
208
+ : "${OLD_PID:?}" "${OLD_STARTED:?}" "${EXPECTED_COMMAND:?}"
209
+ : "${REPO:?}" "${NODE:?}" "${WORKING_DIR:?}" "${WEB_PORT:?}" "${LOG_FILE:?}"
210
+ cd "$REPO"
211
+ exec 3>>"$DEPLOY_DIR/controller.log"
212
+ [ "$(ps -p "$OLD_PID" -o lstart=)" = "$OLD_STARTED" ]
213
+ [ "$(ps -p "$OLD_PID" -o command=)" = "$EXPECTED_COMMAND" ]
214
+ printf '%s stopping pid=%s\n' "$(date -u +%FT%TZ)" "$OLD_PID" >&3
215
+ kill -TERM "$OLD_PID"
216
+ deadline=$((SECONDS + 30))
217
+ while kill -0 "$OLD_PID" 2>/dev/null; do
218
+ if (( SECONDS >= deadline )); then
219
+ echo 'Shutdown timeout; no force-kill and no second process started' >&3
220
+ exit 1
221
+ fi
222
+ sleep 0.1
223
+ done
224
+ [ ! -e "$WORKING_DIR/durable/owner.lock" ]
225
+ port_status=0
226
+ lsof -nP -iTCP:"$WEB_PORT" -sTCP:LISTEN -t > /dev/null || port_status=$?
227
+ case "$port_status" in
228
+ 0) echo 'Port still occupied; refusing startup' >&3; exit 1 ;;
229
+ 1) ;; # No listener.
230
+ *) echo 'Cannot verify port ownership; refusing startup' >&3; exit 1 ;;
231
+ esac
232
+ printf '%s old process exited; ownership released\n' "$(date -u +%FT%TZ)" >&3
233
+ printf '%s\n' "$$" > "$DEPLOY_DIR/new.pid"
234
+ exec 3>&-
235
+ exec "$NODE" --use-env-proxy --experimental-strip-types src/cli.ts serve >> "$LOG_FILE" 2>&1
236
+ ```
237
+
238
+ Launch it once with `chmod 700` and `open -a Terminal "$DEPLOY_DIR/restart.command"`
239
+ (`osascript` `do script` times out on claw). The separate Terminal keeps the
240
+ controller alive when the initiating agent's own turn is interrupted. For a
241
+ launchd controller, use that job's lifecycle instead; never `launchctl kickstart -k`
242
+ from inside the service.
243
+
244
+ ### 4. Verify
245
+
246
+ Read `controller.log`, `new.pid`, the service log and the listener first. Never
247
+ launch the controller twice before the first outcome is known. Require:
248
+
249
+ 1. Old PID gone; exactly one new process and listener, cwd in the release
250
+ directory.
251
+ 2. Fresh `Application starting`, `Agent ready`, `Web server listening`,
252
+ `Messaging ready` and `Application started`; no startup error.
253
+ 3. Root and changed endpoints return 200; check UI changes in a browser.
254
+ 4. Interrupted work continues; scheduler deadlines are unchanged; receipt
255
+ counts change only by new sends and sends interrupted by the stop.
256
+
257
+ On a timeout, lock, occupied port or startup failure, stop and report it. Do not
258
+ force-kill, steal the lock, reinstall dependencies or restore a workspace snapshot.
259
+
260
+ Routine deployments do not run the live smoke. Only when the owner authorizes a
261
+ paid model request and a real message, run
262
+ `node --experimental-strip-types ops/smoke.ts --live --chat CHAT_GUID`: it checks
263
+ `/health/model`, waits for the operator's message from the device and reports
264
+ the new reply's receipt. Never resend a reply that is not `sent`.
265
+ Record the deployment (time, commit, PID, checks, unverified limits) with
266
+ `pi-memory`, delete the `/tmp` controller directory, and report **deployed and
267
+ verified** or the blocker. Release gates and evidence live in GitHub #33.
@@ -0,0 +1,28 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
+ import Database from "better-sqlite3";
9
+ import sharp from "sharp";
10
+
11
+ assert.equal(process.platform, "darwin", "Messages transport requires macOS");
12
+ const version = process.versions.node.split(".").map(Number);
13
+ assert.ok(version[0] > 22 || (version[0] === 22 && (version[1] > 22 || (version[1] === 22 && version[2] >= 2))), "Node 22.22.2 or newer is required");
14
+ const database = new Database(":memory:");
15
+ try { assert.equal(database.prepare("SELECT 1 AS value").get().value, 1); } finally { database.close(); }
16
+ assert.equal((await sharp({ create: { width: 1, height: 1, channels: 3, background: "white" } }).jpeg().toBuffer()).length > 0, true);
17
+ const browserState = await mkdtemp(join(tmpdir(), "imessage-browser-preflight-"));
18
+ try {
19
+ // doctor can report MISSING with exit 0; --version must reach the installed runtime.
20
+ await promisify(execFile)(process.env.PI_BROWSER_CLI_PATH ?? join(getAgentDir(), "bin", "pi-browser"), ["--version"], {
21
+ env: { ...process.env, PI_BROWSER_HOME: browserState }, timeout: 10000, maxBuffer: 65536,
22
+ });
23
+ } catch (error) {
24
+ throw new Error("Browser CLI readiness failed; install the pinned runtime with pi-browser setup", { cause: error });
25
+ } finally {
26
+ await rm(browserState, { recursive: true, force: true });
27
+ }
28
+ console.log("Preflight passed: macOS, Node, native SQLite/image dependencies and browser CLI runtime. No model, Messages or production workspace was accessed.");
package/package.json CHANGED
@@ -1,66 +1,54 @@
1
1
  {
2
2
  "name": "@kingcrab/pi-imessage",
3
- "version": "0.0.42",
4
- "description": "AI-powered iMessage bot using pi coding agent — understands images, quoted replies, and conversation context",
5
- "type": "module",
3
+ "version": "1.0.0",
4
+ "description": "A minimal, self-managing iMessage bot powered by Pi Durable",
6
5
  "license": "MIT",
7
- "author": "Henry Zhu",
8
- "homepage": "https://github.com/daya0576/pi-imessage#readme",
9
- "bugs": "https://github.com/daya0576/pi-imessage/issues",
10
- "keywords": [
11
- "imessage",
12
- "ai",
13
- "chatbot",
14
- "pi",
15
- "macos"
16
- ],
17
- "bin": {
18
- "pi-imessage": "./dist/cli.js"
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/daya0576/pi-imessage.git"
19
9
  },
20
10
  "files": [
21
11
  "dist",
22
- "!dist/__tests__",
23
- "README.md",
24
- "LICENSE"
12
+ "docs",
13
+ "examples",
14
+ "ops/README.md",
15
+ "ops/preflight.mjs"
25
16
  ],
26
17
  "publishConfig": {
27
18
  "access": "public",
28
19
  "provenance": true
29
20
  },
30
- "repository": {
31
- "type": "git",
32
- "url": "git+https://github.com/daya0576/pi-imessage.git"
21
+ "type": "module",
22
+ "bin": {
23
+ "pi-imessage": "dist/cli.js"
33
24
  },
34
25
  "engines": {
35
- "node": ">=20.0.0"
26
+ "node": ">=22.22.2"
36
27
  },
37
28
  "scripts": {
38
- "build": "tsc && mkdir -p dist/web/templates && cp src/web/templates/* dist/web/templates/",
39
- "prepublishOnly": "npm run build",
40
- "dev": "NODE_OPTIONS='--use-env-proxy' tsx watch src/main.ts",
41
- "start": "NODE_OPTIONS='--use-env-proxy' tsx src/main.ts",
42
- "test": "vitest run",
43
- "typecheck": "tsc --noEmit",
44
- "lint": "biome check .",
45
- "format": "biome format --write .",
46
- "check": "tsc --noEmit && biome check ."
29
+ "build": "tsc -p tsconfig.build.json && cp src/web/app.js src/web/style.css dist/web/",
30
+ "prepack": "npm run build",
31
+ "test:package": "npm run build && node ops/package-smoke.mjs",
32
+ "start": "node --use-env-proxy --experimental-strip-types src/cli.ts serve",
33
+ "check": "tsc --noEmit && biome check .",
34
+ "test": "vitest run"
47
35
  },
48
36
  "dependencies": {
49
- "@earendil-works/pi-agent-core": "^0.80.6",
50
- "@earendil-works/pi-ai": "^0.80.6",
51
- "@earendil-works/pi-coding-agent": "^0.80.6",
52
- "better-sqlite3": "^12.6.2",
53
- "dotenv": "^17.3.1",
54
- "eta": "^4.5.1"
37
+ "@earendil-works/chord": "1.0.2",
38
+ "@earendil-works/pi-ai": "1.0.2",
39
+ "@earendil-works/pi-coding-agent": "1.0.2",
40
+ "@earendil-works/pi-durable": "1.0.2",
41
+ "better-sqlite3": "13.0.3",
42
+ "defuddle": "0.19.4",
43
+ "sharp": "0.35.5",
44
+ "undici": "8.10.2",
45
+ "unpdf": "1.8.1"
55
46
  },
56
47
  "devDependencies": {
57
- "@biomejs/biome": "^1.9.4",
58
- "@types/better-sqlite3": "^7.6.13",
59
- "@types/node": "^22.0.0",
60
- "@types/sharp": "^0.31.1",
61
- "@vitest/coverage-v8": "^3.2.4",
62
- "tsx": "^4.0.0",
63
- "typescript": "^5.7.3",
64
- "vitest": "^3.0.0"
48
+ "@biomejs/biome": "2.5.15",
49
+ "@types/better-sqlite3": "7.6.13",
50
+ "@types/node": "22.20.5",
51
+ "typescript": "7.0.2",
52
+ "vitest": "5.0.3"
65
53
  }
66
54
  }
package/dist/agent.d.ts DELETED
@@ -1,60 +0,0 @@
1
- /**
2
- * Agent module — text-in / text-out AI processor per iMessage chat.
3
- *
4
- * Each chat gets a lazily-created AgentSession with persistent context
5
- * (context.jsonl per chat directory).
6
- *
7
- * Concurrency: callers must serialize messages for the same chat externally
8
- * (imessage.ts does this via per-chat promise chains). Different chats run
9
- * concurrently.
10
- *
11
- * Model: uses ~/.pi/agent/ defaults (via createAgentSession).
12
- */
13
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
- import type { AgentReply, IncomingMessage } from "./types.js";
15
- /**
16
- * Abort a prompt only after this much continuous inactivity. Any agent event —
17
- * including streamed model output and tool start/end events — refreshes it.
18
- */
19
- export declare const AGENT_IDLE_TIMEOUT_MS: number;
20
- /** Absolute safety ceiling that activity cannot extend. */
21
- export declare const AGENT_MAX_PROMPT_DURATION_MS: number;
22
- export declare const AUTO_COMPACT_TOKEN_THRESHOLD: number;
23
- /** Enable OpenAI priority processing without enabling user-discovered extensions. */
24
- export declare function openAiCodexFastExtension(pi: ExtensionAPI): void;
25
- export interface AgentManagerConfig {
26
- workingDir: string;
27
- }
28
- /**
29
- * Race an async operation against a wall-clock timeout.
30
- *
31
- * If `operation()` settles first, its result (or rejection) is passed through
32
- * and the timer is cleared. If the timeout fires first, `onTimeout()` is invoked
33
- * (best-effort — e.g. to abort the underlying request) and the returned promise
34
- * rejects with a timeout Error. A failure inside `onTimeout()` never masks the
35
- * timeout rejection.
36
- *
37
- * Note: this does not cancel `operation()` itself — cancellation must happen via
38
- * `onTimeout` (the SDK exposes `AgentSession.abort()`). The timeout rejects
39
- * immediately without waiting for cancellation, because a stuck abort must not
40
- * keep the caller blocked.
41
- */
42
- export declare function runWithTimeout<T>(operation: () => Promise<T>, onTimeout: () => Promise<void> | void, timeoutMs: number): Promise<T>;
43
- export type ActivityTimeoutKind = "idle" | "max_duration";
44
- /**
45
- * Race an operation against a sliding inactivity timeout and a separate hard
46
- * duration ceiling. Calling `markActivity()` refreshes only the idle timer.
47
- */
48
- export declare function runWithActivityTimeout<T>(operation: (markActivity: () => void) => Promise<T>, onTimeout: (kind: ActivityTimeoutKind) => Promise<void> | void, idleTimeoutMs: number, maxDurationMs: number): Promise<T>;
49
- export declare function createAgentManager(config: AgentManagerConfig): Promise<{
50
- processMessage: (msg: IncomingMessage, handler: (reply: AgentReply) => Promise<void>, options?: {
51
- streamingBehavior?: "steer" | "followUp";
52
- }) => Promise<void>;
53
- newSession: (chatGuid: string) => Promise<void>;
54
- getSessionStatus: (chatGuid: string) => Promise<string>;
55
- reload: (chatGuid: string) => Promise<void>;
56
- stop: (chatGuid: string) => Promise<void>;
57
- compact: (chatGuid: string, customInstructions?: string) => Promise<string>;
58
- }>;
59
- export type AgentManager = Pick<Awaited<ReturnType<typeof createAgentManager>>, "processMessage" | "newSession" | "getSessionStatus" | "reload" | "stop" | "compact">;
60
- //# sourceMappingURL=agent.d.ts.map