aiden-runtime 4.1.5 → 4.5.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 (163) hide show
  1. package/README.md +250 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +351 -53
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +138 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/help.js +7 -0
  12. package/dist/cli/v4/commands/index.js +20 -1
  13. package/dist/cli/v4/commands/runs.js +203 -0
  14. package/dist/cli/v4/commands/sandbox.js +48 -0
  15. package/dist/cli/v4/commands/suggestions.js +68 -0
  16. package/dist/cli/v4/commands/tce.js +41 -0
  17. package/dist/cli/v4/commands/trigger.js +378 -0
  18. package/dist/cli/v4/commands/update.js +95 -3
  19. package/dist/cli/v4/daemonAgentBuilder.js +142 -0
  20. package/dist/cli/v4/defaultSoul.js +1 -1
  21. package/dist/cli/v4/display/capabilityCard.js +26 -0
  22. package/dist/cli/v4/display.js +18 -8
  23. package/dist/cli/v4/replyRenderer.js +31 -23
  24. package/dist/cli/v4/updateBootPrompt.js +170 -0
  25. package/dist/core/playwrightBridge.js +129 -0
  26. package/dist/core/v4/aidenAgent.js +308 -4
  27. package/dist/core/v4/browserState.js +436 -0
  28. package/dist/core/v4/checkpoint.js +79 -0
  29. package/dist/core/v4/daemon/bootstrap.js +604 -0
  30. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  31. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  32. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  33. package/dist/core/v4/daemon/cron/migration.js +199 -0
  34. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  35. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  36. package/dist/core/v4/daemon/db/connection.js +106 -0
  37. package/dist/core/v4/daemon/db/migrations.js +296 -0
  38. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  39. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  40. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  41. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  42. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  43. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  44. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  45. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  46. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  47. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  48. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  49. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  50. package/dist/core/v4/daemon/drain.js +156 -0
  51. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  52. package/dist/core/v4/daemon/health.js +159 -0
  53. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  54. package/dist/core/v4/daemon/index.js +179 -0
  55. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  56. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  57. package/dist/core/v4/daemon/restartCode.js +32 -0
  58. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  59. package/dist/core/v4/daemon/runStore.js +114 -0
  60. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  61. package/dist/core/v4/daemon/signals.js +50 -0
  62. package/dist/core/v4/daemon/supervisor.js +272 -0
  63. package/dist/core/v4/daemon/triggerBus.js +279 -0
  64. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  65. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  66. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  67. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  68. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  69. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  70. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  71. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  72. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  73. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  74. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  75. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  76. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  77. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  78. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  79. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  80. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  81. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  82. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  83. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  84. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  85. package/dist/core/v4/daemon/types.js +15 -0
  86. package/dist/core/v4/dockerSession.js +461 -0
  87. package/dist/core/v4/dryRun.js +117 -0
  88. package/dist/core/v4/failureClassifier.js +779 -0
  89. package/dist/core/v4/recoveryReport.js +449 -0
  90. package/dist/core/v4/runtimeToggles.js +187 -0
  91. package/dist/core/v4/sandboxConfig.js +285 -0
  92. package/dist/core/v4/sandboxFs.js +316 -0
  93. package/dist/core/v4/suggestionCatalog.js +41 -0
  94. package/dist/core/v4/suggestionEngine.js +210 -0
  95. package/dist/core/v4/toolRegistry.js +18 -0
  96. package/dist/core/v4/turnState.js +587 -0
  97. package/dist/core/v4/update/checkUpdate.js +63 -3
  98. package/dist/core/v4/update/installMethodDetect.js +115 -0
  99. package/dist/core/v4/update/registryClient.js +121 -0
  100. package/dist/core/v4/update/skipState.js +75 -0
  101. package/dist/core/v4/verifier.js +448 -0
  102. package/dist/core/version.js +1 -1
  103. package/dist/tools/v4/browser/_observer.js +224 -0
  104. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  105. package/dist/tools/v4/browser/browserClick.js +18 -1
  106. package/dist/tools/v4/browser/browserClose.js +18 -1
  107. package/dist/tools/v4/browser/browserExtract.js +5 -1
  108. package/dist/tools/v4/browser/browserFill.js +17 -1
  109. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  110. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  111. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  112. package/dist/tools/v4/browser/browserScroll.js +18 -1
  113. package/dist/tools/v4/browser/browserType.js +17 -1
  114. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  115. package/dist/tools/v4/executeCode.js +1 -0
  116. package/dist/tools/v4/files/fileCopy.js +56 -2
  117. package/dist/tools/v4/files/fileDelete.js +38 -1
  118. package/dist/tools/v4/files/fileList.js +12 -1
  119. package/dist/tools/v4/files/fileMove.js +59 -2
  120. package/dist/tools/v4/files/filePatch.js +43 -1
  121. package/dist/tools/v4/files/fileRead.js +12 -1
  122. package/dist/tools/v4/files/fileWrite.js +41 -1
  123. package/dist/tools/v4/index.js +71 -58
  124. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  125. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  126. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  127. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  128. package/dist/tools/v4/process/processKill.js +19 -0
  129. package/dist/tools/v4/process/processList.js +1 -0
  130. package/dist/tools/v4/process/processLogRead.js +1 -0
  131. package/dist/tools/v4/process/processSpawn.js +13 -0
  132. package/dist/tools/v4/process/processWait.js +1 -0
  133. package/dist/tools/v4/sessions/recallSession.js +1 -0
  134. package/dist/tools/v4/sessions/sessionList.js +1 -0
  135. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  136. package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
  137. package/dist/tools/v4/skills/skillManage.js +13 -0
  138. package/dist/tools/v4/skills/skillView.js +1 -0
  139. package/dist/tools/v4/skills/skillsList.js +1 -0
  140. package/dist/tools/v4/subagent/subagentFanout.js +1 -0
  141. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  142. package/dist/tools/v4/system/appClose.js +13 -0
  143. package/dist/tools/v4/system/appInput.js +13 -0
  144. package/dist/tools/v4/system/appLaunch.js +13 -0
  145. package/dist/tools/v4/system/clipboardRead.js +1 -0
  146. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  147. package/dist/tools/v4/system/mediaKey.js +12 -0
  148. package/dist/tools/v4/system/mediaSessions.js +1 -0
  149. package/dist/tools/v4/system/mediaTransport.js +13 -0
  150. package/dist/tools/v4/system/naturalEvents.js +1 -0
  151. package/dist/tools/v4/system/nowPlaying.js +1 -0
  152. package/dist/tools/v4/system/osProcessList.js +1 -0
  153. package/dist/tools/v4/system/screenshot.js +1 -0
  154. package/dist/tools/v4/system/systemInfo.js +1 -0
  155. package/dist/tools/v4/system/volumeSet.js +17 -0
  156. package/dist/tools/v4/terminal/shellExec.js +81 -9
  157. package/dist/tools/v4/web/deepResearch.js +1 -0
  158. package/dist/tools/v4/web/openUrl.js +1 -0
  159. package/dist/tools/v4/web/webFetch.js +1 -0
  160. package/dist/tools/v4/web/webPage.js +1 -0
  161. package/dist/tools/v4/web/webSearch.js +1 -0
  162. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  163. package/package.json +7 -1
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/fileWatcher.ts — v4.5 Phase 2: the
10
+ * file-watcher trigger source.
11
+ *
12
+ * Wires chokidar 4.x into the Phase 1 trigger bus. One module-level
13
+ * factory `createFileWatcher(spec)` returns a handle that:
14
+ * - subscribes to add/change/unlink events
15
+ * - filters via the glob matcher (default ignores + spec globs)
16
+ * - debounces per-path (default 750ms)
17
+ * - settles via a stat loop (default 1s) — second layer on top
18
+ * of chokidar's `awaitWriteFinish`
19
+ * - upserts a file_observations row + inserts a trigger_event
20
+ * under the bus's UNIQUE(source, idempotency_key) dedup
21
+ * - registers itself in the resourceRegistry for shutdown reap
22
+ * - exposes `stats()` for /metrics + diagnostics
23
+ *
24
+ * Backpressure: per-watcher queue depth (default 100). When full,
25
+ * new events DROP with a log + stats counter bump (Q-P2-5 default).
26
+ * Pausing chokidar would cause a resync-burst on resume — drop-new
27
+ * is the correct trade-off.
28
+ */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.createFileWatcher = createFileWatcher;
34
+ const chokidar_1 = __importDefault(require("chokidar"));
35
+ const node_path_1 = __importDefault(require("node:path"));
36
+ const node_fs_1 = __importDefault(require("node:fs"));
37
+ const node_crypto_1 = __importDefault(require("node:crypto"));
38
+ const globMatcher_1 = require("./globMatcher");
39
+ const fsIdentity_1 = require("./fsIdentity");
40
+ const settleStat_1 = require("./settleStat");
41
+ const noopLog = (_level, _msg) => undefined;
42
+ function createFileWatcher(opts) {
43
+ const { watcherId, spec, triggerBus, obsStore, registry } = opts;
44
+ const log = opts.log ?? noopLog;
45
+ const matcher = (0, globMatcher_1.compileGlobMatcher)({
46
+ includeGlobs: spec.includeGlobs,
47
+ excludeGlobs: spec.excludeGlobs,
48
+ ignoreTemp: spec.ignoreTemp,
49
+ });
50
+ // Per-path pending queue. Map key = absPath; one entry per path.
51
+ const queue = new Map();
52
+ const acceptedEventTypes = new Set(spec.eventTypes);
53
+ let paused = false;
54
+ const stats = {
55
+ queueDepth: 0,
56
+ emitted: 0,
57
+ coalesced: 0,
58
+ skipped: 0,
59
+ dropped: 0,
60
+ overflowed: false,
61
+ lastError: null,
62
+ };
63
+ // ── chokidar setup ──────────────────────────────────────────────────────
64
+ const watcher = chokidar_1.default.watch(spec.paths, {
65
+ persistent: true,
66
+ ignoreInitial: true, // boot-time reconciliation runs separately
67
+ depth: spec.recursive ? undefined : 0,
68
+ awaitWriteFinish: {
69
+ stabilityThreshold: Math.max(50, spec.settleMs),
70
+ pollInterval: 100,
71
+ },
72
+ usePolling: spec.polling?.enabled === true,
73
+ interval: spec.polling?.intervalMs,
74
+ binaryInterval: spec.polling?.binaryIntervalMs,
75
+ // chokidar 4.x removed built-in globs — we filter via picomatch
76
+ // in handleEvent below.
77
+ });
78
+ // ── core event handler ──────────────────────────────────────────────────
79
+ const handleEvent = (eventType, absPath) => {
80
+ if (paused)
81
+ return;
82
+ if (!acceptedEventTypes.has(eventType)) {
83
+ stats.skipped += 1;
84
+ return;
85
+ }
86
+ if (!matcher.match(absPath)) {
87
+ stats.skipped += 1;
88
+ return;
89
+ }
90
+ // Unlink bypasses debounce + settle — the file is gone.
91
+ if (eventType === 'unlink') {
92
+ void emit(absPath, 'unlink', null);
93
+ return;
94
+ }
95
+ // Coalesce repeated events for the same path within the debounce window.
96
+ const existing = queue.get(absPath);
97
+ if (existing) {
98
+ existing.coalesced += 1;
99
+ // Reset the debounce timer.
100
+ if (existing.timer)
101
+ clearTimeout(existing.timer);
102
+ existing.timer = setTimeout(() => { void flush(absPath); }, spec.debounceMs);
103
+ if (typeof existing.timer.unref === 'function')
104
+ existing.timer.unref();
105
+ return;
106
+ }
107
+ if (queue.size >= spec.maxQueueDepth) {
108
+ stats.dropped += 1;
109
+ if (!stats.overflowed) {
110
+ stats.overflowed = true;
111
+ log('warn', `[file-watcher] queue overflow at ${watcherId} — dropping events (depth=${queue.size}, max=${spec.maxQueueDepth})`);
112
+ }
113
+ return;
114
+ }
115
+ const entry = {
116
+ absPath,
117
+ eventType,
118
+ enqueuedAt: Date.now(),
119
+ coalesced: 0,
120
+ timer: null,
121
+ };
122
+ entry.timer = setTimeout(() => { void flush(absPath); }, spec.debounceMs);
123
+ if (entry.timer && typeof entry.timer.unref === 'function')
124
+ entry.timer.unref();
125
+ queue.set(absPath, entry);
126
+ stats.queueDepth = queue.size;
127
+ };
128
+ // ── flush + settle + emit ───────────────────────────────────────────────
129
+ const flush = async (absPath) => {
130
+ const entry = queue.get(absPath);
131
+ if (!entry)
132
+ return;
133
+ queue.delete(absPath);
134
+ stats.queueDepth = queue.size;
135
+ if (stats.overflowed && queue.size <= Math.floor(spec.maxQueueDepth / 2)) {
136
+ stats.overflowed = false;
137
+ log('info', `[file-watcher] queue drained at ${watcherId}`);
138
+ }
139
+ // Settle: stat repeatedly until stable.
140
+ const stable = await (0, settleStat_1.settleStat)(absPath, {
141
+ intervalMs: Math.max(50, spec.settleMs),
142
+ maxSettleMs: spec.maxSettleMs,
143
+ });
144
+ if (stable === null) {
145
+ // File vanished mid-settle; treat as unlink.
146
+ void emit(absPath, 'unlink', null, entry.coalesced);
147
+ return;
148
+ }
149
+ void emit(absPath, entry.eventType, stable, entry.coalesced);
150
+ };
151
+ const emit = async (absPath, eventType, stable, coalescedDelta = 0) => {
152
+ try {
153
+ const fileKey = (0, fsIdentity_1.computeFileKey)(absPath);
154
+ const size = stable?.size ?? null;
155
+ const mtimeMs = stable?.mtimeMs ?? Date.now();
156
+ const contentHash = spec.contentHash && eventType !== 'unlink' && stable !== null
157
+ ? await sha256OfFile(absPath)
158
+ : null;
159
+ const observationId = obsStore.upsert({
160
+ watcherId,
161
+ absPath,
162
+ fileKey,
163
+ size,
164
+ mtimeMs,
165
+ contentHash,
166
+ eventType,
167
+ coalescedDelta,
168
+ });
169
+ const idempotencyKey = `${absPath}::${mtimeMs}::${size ?? 'null'}`;
170
+ const insertResult = triggerBus.insert({
171
+ source: 'file',
172
+ sourceKey: watcherId,
173
+ idempotencyKey,
174
+ payload: {
175
+ absPath,
176
+ eventType,
177
+ mtime: mtimeMs,
178
+ size,
179
+ fileKey,
180
+ contentHash,
181
+ watcherId,
182
+ },
183
+ });
184
+ obsStore.markProcessed({
185
+ observationId,
186
+ eventId: insertResult.id,
187
+ status: 'pending',
188
+ });
189
+ stats.emitted += 1;
190
+ stats.coalesced += coalescedDelta;
191
+ }
192
+ catch (e) {
193
+ const msg = e instanceof Error ? e.message : String(e);
194
+ stats.lastError = msg;
195
+ log('error', `[file-watcher] emit failed for ${absPath}: ${msg}`);
196
+ }
197
+ };
198
+ // ── wire chokidar events ────────────────────────────────────────────────
199
+ watcher.on('add', (p) => handleEvent('add', node_path_1.default.resolve(p)));
200
+ watcher.on('change', (p) => handleEvent('change', node_path_1.default.resolve(p)));
201
+ watcher.on('unlink', (p) => handleEvent('unlink', node_path_1.default.resolve(p)));
202
+ watcher.on('error', (err) => {
203
+ const msg = err instanceof Error ? err.message : String(err);
204
+ stats.lastError = msg;
205
+ log('error', `[file-watcher] chokidar error at ${watcherId}: ${msg}`);
206
+ });
207
+ // ── resource registry ───────────────────────────────────────────────────
208
+ const close = async () => {
209
+ paused = true;
210
+ // Cancel pending debounce timers.
211
+ for (const e of queue.values()) {
212
+ if (e.timer)
213
+ clearTimeout(e.timer);
214
+ }
215
+ queue.clear();
216
+ stats.queueDepth = 0;
217
+ try {
218
+ await watcher.close();
219
+ }
220
+ catch (e) {
221
+ log('warn', `[file-watcher] close error: ${e instanceof Error ? e.message : String(e)}`);
222
+ }
223
+ };
224
+ const resourceId = registry.register({
225
+ kind: 'file_watcher',
226
+ owner: watcherId,
227
+ metadata: { paths: spec.paths },
228
+ close,
229
+ });
230
+ return {
231
+ watcherId,
232
+ resourceId,
233
+ pause() { paused = true; },
234
+ resume() { paused = false; },
235
+ close,
236
+ stats() { return { ...stats }; },
237
+ };
238
+ }
239
+ // ── SHA-256 of file contents (opt-in via spec.contentHash) ─────────────────
240
+ function sha256OfFile(absPath) {
241
+ return new Promise((resolve) => {
242
+ try {
243
+ const h = node_crypto_1.default.createHash('sha256');
244
+ const stream = node_fs_1.default.createReadStream(absPath);
245
+ stream.on('data', (chunk) => { h.update(chunk); });
246
+ stream.on('error', () => resolve(null));
247
+ stream.on('end', () => resolve(h.digest('hex')));
248
+ }
249
+ catch {
250
+ resolve(null);
251
+ }
252
+ });
253
+ }
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/fileWatcherSpec.ts — v4.5 Phase 2.
10
+ *
11
+ * Trigger-spec shape stored in `triggers.spec_json` for
12
+ * `source='file'`. Pure types + parse helpers; no I/O.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DEFAULT_FILE_WATCHER_SPEC = void 0;
16
+ exports.parseFileWatcherSpec = parseFileWatcherSpec;
17
+ exports.DEFAULT_FILE_WATCHER_SPEC = {
18
+ recursive: true,
19
+ eventTypes: ['add', 'change', 'unlink'],
20
+ debounceMs: 750,
21
+ settleMs: 1000,
22
+ maxSettleMs: 30000,
23
+ maxQueueDepth: 100,
24
+ ignoreTemp: true,
25
+ contentHash: false,
26
+ reconcile: 'skip_existing',
27
+ };
28
+ /**
29
+ * Parse + fill in defaults. Throws when `paths` is missing/empty.
30
+ * Tolerates extra keys (forward-compatible).
31
+ */
32
+ function parseFileWatcherSpec(raw) {
33
+ const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
34
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
35
+ throw new Error('FileWatcherSpec: input must be an object');
36
+ }
37
+ const o = obj;
38
+ const paths = Array.isArray(o.paths) ? o.paths.filter((p) => typeof p === 'string') : [];
39
+ if (paths.length === 0) {
40
+ throw new Error('FileWatcherSpec: at least one path required');
41
+ }
42
+ return {
43
+ paths,
44
+ recursive: typeof o.recursive === 'boolean' ? o.recursive : exports.DEFAULT_FILE_WATCHER_SPEC.recursive,
45
+ includeGlobs: Array.isArray(o.includeGlobs) ? o.includeGlobs.filter((s) => typeof s === 'string') : undefined,
46
+ excludeGlobs: Array.isArray(o.excludeGlobs) ? o.excludeGlobs.filter((s) => typeof s === 'string') : undefined,
47
+ eventTypes: sanitizeEventTypes(o.eventTypes),
48
+ debounceMs: sanitizeNum(o.debounceMs, exports.DEFAULT_FILE_WATCHER_SPEC.debounceMs, 0),
49
+ settleMs: sanitizeNum(o.settleMs, exports.DEFAULT_FILE_WATCHER_SPEC.settleMs, 0),
50
+ maxSettleMs: sanitizeNum(o.maxSettleMs, exports.DEFAULT_FILE_WATCHER_SPEC.maxSettleMs, 0),
51
+ maxQueueDepth: sanitizeNum(o.maxQueueDepth, exports.DEFAULT_FILE_WATCHER_SPEC.maxQueueDepth, 1),
52
+ ignoreTemp: typeof o.ignoreTemp === 'boolean' ? o.ignoreTemp : exports.DEFAULT_FILE_WATCHER_SPEC.ignoreTemp,
53
+ contentHash: typeof o.contentHash === 'boolean' ? o.contentHash : exports.DEFAULT_FILE_WATCHER_SPEC.contentHash,
54
+ reconcile: sanitizeReconcile(o.reconcile),
55
+ polling: sanitizePolling(o.polling),
56
+ promptTemplate: typeof o.promptTemplate === 'string' ? o.promptTemplate : undefined,
57
+ };
58
+ }
59
+ function sanitizeNum(v, fallback, min) {
60
+ if (typeof v !== 'number' || !Number.isFinite(v) || v < min)
61
+ return fallback;
62
+ return v;
63
+ }
64
+ function sanitizeEventTypes(v) {
65
+ if (!Array.isArray(v))
66
+ return [...exports.DEFAULT_FILE_WATCHER_SPEC.eventTypes];
67
+ const valid = new Set(['add', 'change', 'unlink']);
68
+ const out = v.filter((s) => typeof s === 'string' && valid.has(s));
69
+ return out.length > 0 ? out : [...exports.DEFAULT_FILE_WATCHER_SPEC.eventTypes];
70
+ }
71
+ function sanitizeReconcile(v) {
72
+ if (v === 'skip_existing' || v === 'process_new_since_last_seen' || v === 'full_rescan') {
73
+ return v;
74
+ }
75
+ return exports.DEFAULT_FILE_WATCHER_SPEC.reconcile;
76
+ }
77
+ function sanitizePolling(v) {
78
+ if (!v || typeof v !== 'object' || Array.isArray(v))
79
+ return undefined;
80
+ const o = v;
81
+ if (typeof o.enabled !== 'boolean' || !o.enabled)
82
+ return undefined;
83
+ return {
84
+ enabled: true,
85
+ intervalMs: typeof o.intervalMs === 'number' && o.intervalMs > 0 ? o.intervalMs : undefined,
86
+ binaryIntervalMs: typeof o.binaryIntervalMs === 'number' && o.binaryIntervalMs > 0 ? o.binaryIntervalMs : undefined,
87
+ };
88
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/fsIdentity.ts — v4.5 Phase 2.
10
+ *
11
+ * Compute a best-effort identity key for a file. Used by
12
+ * `file_observations.file_key` so future v4.5.x revisions can
13
+ * detect renames-within-watcher without re-scanning the world.
14
+ *
15
+ * Platforms:
16
+ * - POSIX: ino (inode) — stable across rename
17
+ * - Windows: fs.statSync({bigint:true}).ino — packs the NTFS
18
+ * file index; stable across rename on the same volume
19
+ *
20
+ * Returns '' on stat failure (permission denied, file vanished).
21
+ * Empty string is acceptable — the (watcher_id, abs_path) UNIQUE
22
+ * index in file_observations is the primary key; file_key is a
23
+ * diagnostic/future-feature column.
24
+ */
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.computeFileKey = computeFileKey;
30
+ const node_fs_1 = __importDefault(require("node:fs"));
31
+ function computeFileKey(absPath) {
32
+ try {
33
+ const st = node_fs_1.default.statSync(absPath, { bigint: true });
34
+ // BigInt ino — stringify so SQLite stores it as TEXT.
35
+ if (typeof st.ino === 'bigint')
36
+ return st.ino.toString();
37
+ return String(st.ino);
38
+ }
39
+ catch {
40
+ return '';
41
+ }
42
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/globMatcher.ts — v4.5 Phase 2.
10
+ *
11
+ * chokidar 4.x removed built-in glob matching. We use `picomatch`
12
+ * (already in Aiden's transitive deps) to filter paths AFTER
13
+ * chokidar emits — same semantics, less bundle weight.
14
+ *
15
+ * Match semantics (applied in order):
16
+ * 1. ignoreTemp default deny list (editor temps, .git/, node_modules/, …)
17
+ * 2. spec.excludeGlobs deny list
18
+ * 3. spec.includeGlobs allow list (default ['**∕*'])
19
+ *
20
+ * Compiled matchers are cached per-spec so we don't recompile on
21
+ * every event.
22
+ */
23
+ var __importDefault = (this && this.__importDefault) || function (mod) {
24
+ return (mod && mod.__esModule) ? mod : { "default": mod };
25
+ };
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.DEFAULT_IGNORE_PATTERNS = void 0;
28
+ exports.normalizeGlobPattern = normalizeGlobPattern;
29
+ exports.compileGlobMatcher = compileGlobMatcher;
30
+ const picomatch_1 = __importDefault(require("picomatch"));
31
+ /** Default ignore patterns when FileWatcherSpec.ignoreTemp = true. */
32
+ exports.DEFAULT_IGNORE_PATTERNS = Object.freeze([
33
+ // editor temps
34
+ '**/*.swp', '**/*.swo', '**/*~',
35
+ '**/.*.swp', '**/.*.swo',
36
+ '**/*.tmp', '**/*.temp', '**/*.part',
37
+ '**/.#*', // emacs lock
38
+ '**/~$*', // MS Office temp
39
+ // OS metadata
40
+ '**/.DS_Store', '**/Thumbs.db', '**/desktop.ini',
41
+ // VCS
42
+ '**/.git/**', '**/.svn/**', '**/.hg/**',
43
+ // dependency / build outputs
44
+ '**/node_modules/**',
45
+ '**/dist/**', '**/build/**', '**/.next/**',
46
+ '**/__pycache__/**', '**/*.pyc',
47
+ '**/.venv/**', '**/venv/**',
48
+ '**/target/**',
49
+ ]);
50
+ /**
51
+ * Normalize a glob pattern so it matches absolute paths sensibly.
52
+ *
53
+ * The user mental model is "`*.txt` matches `.txt` files anywhere",
54
+ * but picomatch's `*` does NOT span path separators — so a bare
55
+ * basename glob like `*.txt` never matches `/some/dir/foo.txt`.
56
+ *
57
+ * Rule: if a pattern doesn't already begin with double-star,
58
+ * a leading slash, a Windows drive letter (`C:`), or contain a
59
+ * `/` directory separator, treat it as a basename pattern and
60
+ * prepend a depth-spanning prefix so it matches at any depth.
61
+ * Patterns that already express locality (containing `/` or
62
+ * starting with double-star) are left alone.
63
+ */
64
+ function normalizeGlobPattern(pat) {
65
+ if (pat.startsWith('**'))
66
+ return pat;
67
+ if (pat.startsWith('/'))
68
+ return pat;
69
+ if (/^[A-Za-z]:/.test(pat))
70
+ return pat; // absolute Windows path
71
+ if (pat.includes('/'))
72
+ return pat;
73
+ return '**/' + pat;
74
+ }
75
+ function compileGlobMatcher(opts) {
76
+ const include = opts.includeGlobs && opts.includeGlobs.length > 0
77
+ ? opts.includeGlobs
78
+ : ['**/*'];
79
+ const exclude = [
80
+ ...(opts.excludeGlobs ?? []),
81
+ ...(opts.ignoreTemp !== false ? exports.DEFAULT_IGNORE_PATTERNS : []),
82
+ ];
83
+ const opt = { dot: true, nocase: process.platform === 'win32' };
84
+ const compile = (p) => (0, picomatch_1.default)(normalizeGlobPattern(p), opt);
85
+ const includeFns = include.map(compile);
86
+ const excludeFns = exclude.map(compile);
87
+ return {
88
+ match(absPath) {
89
+ // Normalize to forward slashes for cross-platform glob matching.
90
+ const norm = absPath.replace(/\\/g, '/');
91
+ for (const fn of excludeFns)
92
+ if (fn(norm))
93
+ return false;
94
+ for (const fn of includeFns)
95
+ if (fn(norm))
96
+ return true;
97
+ return false;
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/daemon/triggers/reconcile.ts — v4.5 Phase 2.
10
+ *
11
+ * Boot-time reconciliation for file-watcher triggers. Walks the
12
+ * watched directory trees (glob-pruning excluded dirs) and decides
13
+ * per-policy whether to emit catch-up events for files that changed
14
+ * while the daemon was down.
15
+ *
16
+ * Three policies (configurable per watcher):
17
+ * - `skip_existing` (default) — walk + stat, write observation
18
+ * rows with last_status='done' and NO trigger_event emission.
19
+ * Future changes after boot emit normally. Matches the IMAP
20
+ * `_seen_uids`-on-connect philosophy referenced in the audit.
21
+ *
22
+ * - `process_new_since_last_seen` — for each file, compare its
23
+ * current mtime to the existing observation row. If newer (or
24
+ * no observation row exists), emit a synthetic 'change' (or
25
+ * 'add') trigger_event. Catch-up mode.
26
+ *
27
+ * - `full_rescan` — emit a synthetic 'add' for every matched
28
+ * file regardless of prior observations. One-shot indexing
29
+ * mode — use with care on large trees.
30
+ */
31
+ var __importDefault = (this && this.__importDefault) || function (mod) {
32
+ return (mod && mod.__esModule) ? mod : { "default": mod };
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.reconcileFileWatcher = reconcileFileWatcher;
36
+ const node_fs_1 = __importDefault(require("node:fs"));
37
+ const node_path_1 = __importDefault(require("node:path"));
38
+ const globMatcher_1 = require("./globMatcher");
39
+ const fsIdentity_1 = require("./fsIdentity");
40
+ const noopLog = (_l, _m) => undefined;
41
+ function reconcileFileWatcher(opts) {
42
+ const { watcherId, spec, triggerBus, obsStore } = opts;
43
+ const log = opts.log ?? noopLog;
44
+ const max = opts.maxEntries ?? 100000;
45
+ const matcher = (0, globMatcher_1.compileGlobMatcher)({
46
+ includeGlobs: spec.includeGlobs,
47
+ excludeGlobs: spec.excludeGlobs,
48
+ ignoreTemp: spec.ignoreTemp,
49
+ });
50
+ const result = {
51
+ walked: 0, matched: 0, recorded: 0, emitted: 0, skipped: 0,
52
+ };
53
+ for (const root of spec.paths) {
54
+ walkDir(root, spec.recursive, (absPath) => {
55
+ result.walked += 1;
56
+ if (result.walked > max)
57
+ return false;
58
+ if (!matcher.match(absPath)) {
59
+ result.skipped += 1;
60
+ return true;
61
+ }
62
+ result.matched += 1;
63
+ const stat = tryStat(absPath);
64
+ if (!stat)
65
+ return true;
66
+ const prev = obsStore.get(watcherId, absPath);
67
+ const fileKey = (0, fsIdentity_1.computeFileKey)(absPath);
68
+ switch (spec.reconcile) {
69
+ case 'skip_existing': {
70
+ // Write observation but do NOT emit a trigger_event.
71
+ const obsId = obsStore.upsert({
72
+ watcherId,
73
+ absPath,
74
+ fileKey,
75
+ size: stat.size,
76
+ mtimeMs: stat.mtimeMs,
77
+ contentHash: null,
78
+ eventType: 'add',
79
+ });
80
+ obsStore.markProcessed({
81
+ observationId: obsId,
82
+ eventId: null,
83
+ status: 'done',
84
+ });
85
+ result.recorded += 1;
86
+ break;
87
+ }
88
+ case 'process_new_since_last_seen': {
89
+ const isNew = !prev;
90
+ const isChanged = prev && (prev.mtimeMs !== stat.mtimeMs || prev.size !== stat.size);
91
+ if (isNew || isChanged) {
92
+ emitFor(opts, watcherId, absPath, isNew ? 'add' : 'change', stat, fileKey);
93
+ result.emitted += 1;
94
+ }
95
+ else {
96
+ // Up-to-date — refresh last_seen_at.
97
+ obsStore.upsert({
98
+ watcherId, absPath, fileKey,
99
+ size: stat.size, mtimeMs: stat.mtimeMs,
100
+ contentHash: null, eventType: 'change',
101
+ });
102
+ result.skipped += 1;
103
+ }
104
+ break;
105
+ }
106
+ case 'full_rescan': {
107
+ emitFor(opts, watcherId, absPath, 'add', stat, fileKey);
108
+ result.emitted += 1;
109
+ break;
110
+ }
111
+ }
112
+ return true;
113
+ }, opts);
114
+ }
115
+ log('info', `[file-watcher] reconcile ${spec.reconcile} for ${watcherId}: walked=${result.walked} matched=${result.matched} recorded=${result.recorded} emitted=${result.emitted}`);
116
+ return result;
117
+ }
118
+ function emitFor(opts, watcherId, absPath, eventType, stat, fileKey) {
119
+ const obsId = opts.obsStore.upsert({
120
+ watcherId,
121
+ absPath,
122
+ fileKey,
123
+ size: stat.size,
124
+ mtimeMs: stat.mtimeMs,
125
+ contentHash: null,
126
+ eventType,
127
+ });
128
+ const ev = opts.triggerBus.insert({
129
+ source: 'file',
130
+ sourceKey: watcherId,
131
+ idempotencyKey: `${absPath}::${stat.mtimeMs}::${stat.size}`,
132
+ payload: {
133
+ absPath,
134
+ eventType,
135
+ mtime: stat.mtimeMs,
136
+ size: stat.size,
137
+ fileKey,
138
+ contentHash: null,
139
+ watcherId,
140
+ reconciled: true,
141
+ },
142
+ });
143
+ opts.obsStore.markProcessed({
144
+ observationId: obsId,
145
+ eventId: ev.id,
146
+ status: 'pending',
147
+ });
148
+ }
149
+ function tryStat(p) {
150
+ try {
151
+ const s = node_fs_1.default.statSync(p);
152
+ if (!s.isFile())
153
+ return null;
154
+ return { size: s.size, mtimeMs: s.mtimeMs };
155
+ }
156
+ catch {
157
+ return null;
158
+ }
159
+ }
160
+ /**
161
+ * Glob-pruning directory walker. Calls `cb(absPath)` for each FILE
162
+ * (not directory) encountered. Returning false from the callback
163
+ * aborts the walk (used for entry caps).
164
+ *
165
+ * Dir-level pruning: when a directory's path matches the ignoreTemp/
166
+ * excludeGlobs patterns, we don't recurse into it — never enumerate
167
+ * `node_modules/` etc.
168
+ */
169
+ function walkDir(root, recursive, cb, opts) {
170
+ const dirMatcher = (0, globMatcher_1.compileGlobMatcher)({
171
+ excludeGlobs: opts.spec.excludeGlobs,
172
+ ignoreTemp: opts.spec.ignoreTemp,
173
+ // Allow EVERYTHING for the directory walker; the exclude list
174
+ // does the pruning. Include filter is applied per-file via the
175
+ // caller's matcher.
176
+ includeGlobs: ['**/*'],
177
+ });
178
+ const stack = [node_path_1.default.resolve(root)];
179
+ while (stack.length > 0) {
180
+ const dir = stack.pop();
181
+ let entries;
182
+ try {
183
+ entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
184
+ }
185
+ catch {
186
+ continue;
187
+ }
188
+ for (const ent of entries) {
189
+ const abs = node_path_1.default.join(dir, ent.name);
190
+ if (ent.isDirectory()) {
191
+ // Prune via the *negated* glob match — a directory whose
192
+ // path does NOT match the exclude/ignoreTemp deny list is
193
+ // walked into. Since includeGlobs = ['**/*'], a dir matches
194
+ // iff no exclude/ignore pattern hit it.
195
+ if (recursive && dirMatcher.match(abs)) {
196
+ stack.push(abs);
197
+ }
198
+ }
199
+ else if (ent.isFile()) {
200
+ const cont = cb(abs);
201
+ if (!cont)
202
+ return;
203
+ }
204
+ }
205
+ }
206
+ }