aiden-runtime 4.9.1 → 4.9.3
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.
- package/README.md +47 -1
- package/dist/cli/v4/aidenPrompt.js +12 -0
- package/dist/cli/v4/chatSession.js +41 -13
- package/dist/cli/v4/commands/channel.js +4 -6
- package/dist/cli/v4/commands/cron.js +6 -1
- package/dist/cli/v4/commands/daemonDoctor.js +5 -5
- package/dist/cli/v4/commands/daemonStatus.js +1 -1
- package/dist/cli/v4/commands/greeter.js +86 -0
- package/dist/cli/v4/commands/help.js +2 -0
- package/dist/cli/v4/commands/index.js +4 -0
- package/dist/cli/v4/commands/mcp.js +2 -2
- package/dist/cli/v4/commands/plugins.js +4 -6
- package/dist/cli/v4/commands/trigger.js +18 -18
- package/dist/cli/v4/confirmPrompt.js +67 -0
- package/dist/cli/v4/greeter/history.js +134 -0
- package/dist/cli/v4/greeter/index.js +147 -0
- package/dist/cli/v4/greeter/scan.js +140 -0
- package/dist/cli/v4/greeter/selectOffer.js +118 -0
- package/dist/cli/v4/greeter/templates.js +51 -0
- package/dist/cli/v4/greeter/types.js +23 -0
- package/dist/core/v4/daemon/db/migrations.js +398 -398
- package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +10 -10
- package/dist/core/v4/daemon/incarnationStore.js +9 -9
- package/dist/core/v4/daemon/runs/attemptStore.js +8 -8
- package/dist/core/v4/daemon/runs/reclaim.js +12 -12
- package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +19 -19
- package/dist/core/v4/daemon/spans/spanStore.js +14 -14
- package/dist/core/v4/daemon/triggerBus.js +61 -61
- package/dist/core/v4/hooks/auditQuery.js +11 -11
- package/dist/core/v4/hooks/dispatcher.js +13 -13
- package/dist/core/v4/hooks/registry.js +8 -8
- package/dist/core/v4/mcp/transport.js +9 -9
- package/dist/core/v4/update/executeInstall.js +29 -18
- package/dist/core/v4/update/recoveryScript.js +70 -0
- package/dist/core/v4/util/spawnCommand.js +151 -0
- package/package.json +1 -1
- package/themes/default.yaml +52 -52
- package/themes/dracula.yaml +32 -32
- package/themes/light.yaml +32 -32
- package/themes/monochrome.yaml +31 -31
- package/themes/tokyo-night.yaml +32 -32
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
* cli/v4/greeter/history.ts — v4.9.3 SLICE 1a.
|
|
10
|
+
*
|
|
11
|
+
* Greeter state persistence. Single JSON file at
|
|
12
|
+
* `<paths.root>/.greeter-history.json` — matches the existing
|
|
13
|
+
* `.first-run-shown` / `.recent-commands.json` precedent rather than
|
|
14
|
+
* carving out a `state/` subdirectory for one file.
|
|
15
|
+
*
|
|
16
|
+
* Three exported helpers:
|
|
17
|
+
* - readHistory → null when the file does not exist (first launch)
|
|
18
|
+
* - writeHistory → atomic via tmp + rename, matches the v4
|
|
19
|
+
* `upsertEnv` pattern
|
|
20
|
+
* - reconcilePending → pure function that walks the history and
|
|
21
|
+
* resolves pending offers (no `response` yet)
|
|
22
|
+
* using passive next-boot signals from the scan
|
|
23
|
+
* result. No fs IO inside; caller writes after.
|
|
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.historyPath = historyPath;
|
|
30
|
+
exports.readHistory = readHistory;
|
|
31
|
+
exports.writeHistory = writeHistory;
|
|
32
|
+
exports.reconcilePending = reconcilePending;
|
|
33
|
+
const node_fs_1 = require("node:fs");
|
|
34
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
35
|
+
const types_1 = require("./types");
|
|
36
|
+
const FILE_NAME = '.greeter-history.json';
|
|
37
|
+
/** Absolute path to the greeter history file. Exported for tests. */
|
|
38
|
+
function historyPath(paths) {
|
|
39
|
+
return node_path_1.default.join(paths.root, FILE_NAME);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read the history file. Returns null when:
|
|
43
|
+
* - the file does not exist (first launch — caller stays silent), OR
|
|
44
|
+
* - the JSON fails to parse (treat as corrupt → start fresh)
|
|
45
|
+
*
|
|
46
|
+
* Returns the parsed object on success. Schema version is checked; an
|
|
47
|
+
* unknown `v` value also returns null so a forward-incompatible file
|
|
48
|
+
* doesn't crash an older Aiden mid-boot. Real schema migrations get
|
|
49
|
+
* their own seam in a future slice.
|
|
50
|
+
*/
|
|
51
|
+
async function readHistory(paths, fsImpl = node_fs_1.promises) {
|
|
52
|
+
try {
|
|
53
|
+
const raw = await fsImpl.readFile(historyPath(paths), 'utf8');
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (parsed?.v !== 1)
|
|
56
|
+
return null;
|
|
57
|
+
return {
|
|
58
|
+
v: 1,
|
|
59
|
+
firstLaunchAt: typeof parsed.firstLaunchAt === 'string' ? parsed.firstLaunchAt : new Date().toISOString(),
|
|
60
|
+
lastGreetingAt: typeof parsed.lastGreetingAt === 'string' ? parsed.lastGreetingAt : new Date().toISOString(),
|
|
61
|
+
lastCwd: typeof parsed.lastCwd === 'string' ? parsed.lastCwd : undefined,
|
|
62
|
+
offers: Array.isArray(parsed.offers) ? parsed.offers : [],
|
|
63
|
+
disabled: parsed.disabled === true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ENOENT or parse error — caller decides what null means.
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Atomically write the history file. tmp + rename so a process crash
|
|
73
|
+
* mid-write never leaves a half-written JSON the next boot trips on.
|
|
74
|
+
* Errors are swallowed at the boundary by callers (orchestrator) so
|
|
75
|
+
* a read-only disk doesn't crash the REPL.
|
|
76
|
+
*/
|
|
77
|
+
async function writeHistory(paths, history, fsImpl = node_fs_1.promises) {
|
|
78
|
+
await fsImpl.mkdir(paths.root, { recursive: true });
|
|
79
|
+
const dst = historyPath(paths);
|
|
80
|
+
const tmp = `${dst}.${process.pid}.tmp`;
|
|
81
|
+
await fsImpl.writeFile(tmp, JSON.stringify(history, null, 2) + '\n', 'utf8');
|
|
82
|
+
await fsImpl.rename(tmp, dst);
|
|
83
|
+
}
|
|
84
|
+
function reconcilePending(input) {
|
|
85
|
+
const { history, installedVersion, now } = input;
|
|
86
|
+
const ageDays = (offeredAt) => (now.getTime() - Date.parse(offeredAt)) / (1000 * 60 * 60 * 24);
|
|
87
|
+
const resolved = history.offers.map((o) => {
|
|
88
|
+
if (o.response)
|
|
89
|
+
return o; // already settled
|
|
90
|
+
// update-available-<targetVersion> — accepted if running >= target.
|
|
91
|
+
if (o.id.startsWith('update-available-')) {
|
|
92
|
+
const target = o.id.slice('update-available-'.length);
|
|
93
|
+
if (semverGte(installedVersion, target)) {
|
|
94
|
+
return { ...o, response: 'accepted' };
|
|
95
|
+
}
|
|
96
|
+
if (ageDays(o.offeredAt) > types_1.DECAY_DAYS_UPDATE) {
|
|
97
|
+
return { ...o, response: 'ignored' };
|
|
98
|
+
}
|
|
99
|
+
return o;
|
|
100
|
+
}
|
|
101
|
+
// Greeting-only offers (no expectedAction) — close immediately on
|
|
102
|
+
// next boot. Decay against future offers of the same id happens at
|
|
103
|
+
// selectOffer time, not here.
|
|
104
|
+
if (!o.expectedAction) {
|
|
105
|
+
return { ...o, response: 'ignored' };
|
|
106
|
+
}
|
|
107
|
+
// Other environment offers with an expectedAction — decay by env
|
|
108
|
+
// window. (Slice 1 has none in this category; v4.10 may add.)
|
|
109
|
+
if (ageDays(o.offeredAt) > types_1.DECAY_DAYS_ENVIRONMENT) {
|
|
110
|
+
return { ...o, response: 'ignored' };
|
|
111
|
+
}
|
|
112
|
+
return o;
|
|
113
|
+
});
|
|
114
|
+
return { ...history, offers: resolved };
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Lightweight semver-`>=` for dot-separated numeric versions. Enough
|
|
118
|
+
* for the v4.X.Y space; does not handle pre-release tags (offer ids
|
|
119
|
+
* never carry them — they're built from `UpdateStatus.latest` which
|
|
120
|
+
* the npm registry returns as a clean release version).
|
|
121
|
+
*/
|
|
122
|
+
function semverGte(a, b) {
|
|
123
|
+
const pa = a.split('.').map((s) => Number(s) || 0);
|
|
124
|
+
const pb = b.split('.').map((s) => Number(s) || 0);
|
|
125
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
126
|
+
const va = pa[i] ?? 0;
|
|
127
|
+
const vb = pb[i] ?? 0;
|
|
128
|
+
if (va > vb)
|
|
129
|
+
return true;
|
|
130
|
+
if (va < vb)
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return true; // equal
|
|
134
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
* cli/v4/greeter/index.ts — v4.9.3 SLICE 1a.
|
|
10
|
+
*
|
|
11
|
+
* Boot-time greeter orchestrator. One shot per process.
|
|
12
|
+
*
|
|
13
|
+
* Behaviour contract (per Phase A/B):
|
|
14
|
+
* 1. SILENT on first-ever launch (history file missing). Writes a
|
|
15
|
+
* fresh v:1 history then returns. The existing renderFirstRunHint
|
|
16
|
+
* owns the first-boot moment.
|
|
17
|
+
* 2. SILENT when history.disabled === true (kill switch).
|
|
18
|
+
* 3. SILENT when no offer wins (nothing noticeable).
|
|
19
|
+
* 4. NEVER throws — internal errors are swallowed; a broken greeter
|
|
20
|
+
* must not crash the REPL.
|
|
21
|
+
* 5. Reconciles pending offers from prior boots against current
|
|
22
|
+
* scan state BEFORE selecting a new offer; the new offer (if any)
|
|
23
|
+
* is appended to history with `response` undefined (pending).
|
|
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.renderGreeter = renderGreeter;
|
|
30
|
+
const node_fs_1 = require("node:fs");
|
|
31
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
32
|
+
const theme_1 = require("../../../core/v4/ui/theme");
|
|
33
|
+
const history_1 = require("./history");
|
|
34
|
+
const scan_1 = require("./scan");
|
|
35
|
+
const selectOffer_1 = require("./selectOffer");
|
|
36
|
+
/**
|
|
37
|
+
* Run the greeter exactly once. Always resolves; never throws.
|
|
38
|
+
*
|
|
39
|
+
* Returns nothing — speech (or silence) is written via display.write.
|
|
40
|
+
* Tests assert against captured display.write calls, NOT a return
|
|
41
|
+
* value (Slice 2 lesson: return-value snapshots prove nothing about
|
|
42
|
+
* what reaches the terminal).
|
|
43
|
+
*/
|
|
44
|
+
async function renderGreeter(opts) {
|
|
45
|
+
try {
|
|
46
|
+
await renderGreeterUnsafe(opts);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Greeter must never crash the REPL. Silent on any internal error.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function renderGreeterUnsafe(opts) {
|
|
53
|
+
const fsImpl = opts.fsImpl ?? node_fs_1.promises;
|
|
54
|
+
const now = opts.now ?? new Date();
|
|
55
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
56
|
+
// ── First-launch path: write fresh history, stay silent --------------
|
|
57
|
+
const existing = await (0, history_1.readHistory)(opts.paths, fsImpl);
|
|
58
|
+
if (existing === null) {
|
|
59
|
+
const fresh = {
|
|
60
|
+
v: 1,
|
|
61
|
+
firstLaunchAt: now.toISOString(),
|
|
62
|
+
lastGreetingAt: now.toISOString(),
|
|
63
|
+
lastCwd: cwd,
|
|
64
|
+
offers: [],
|
|
65
|
+
disabled: false,
|
|
66
|
+
};
|
|
67
|
+
await (0, history_1.writeHistory)(opts.paths, fresh, fsImpl);
|
|
68
|
+
return; // SILENT — renderFirstRunHint owns this moment
|
|
69
|
+
}
|
|
70
|
+
// ── Reconcile pending offers from prior boots ------------------------
|
|
71
|
+
const scanForReconcile = await (0, scan_1.runScans)({
|
|
72
|
+
paths: opts.paths,
|
|
73
|
+
cwd,
|
|
74
|
+
now,
|
|
75
|
+
version: opts.version,
|
|
76
|
+
history: existing,
|
|
77
|
+
fsImpl,
|
|
78
|
+
});
|
|
79
|
+
const reconciled = (0, history_1.reconcilePending)({
|
|
80
|
+
history: existing,
|
|
81
|
+
scan: scanForReconcile,
|
|
82
|
+
installedVersion: opts.version,
|
|
83
|
+
now,
|
|
84
|
+
});
|
|
85
|
+
// ── Pick at most one offer to render this boot ----------------------
|
|
86
|
+
const distillation = await loadLatestDistillation(opts.paths, fsImpl);
|
|
87
|
+
const offer = (0, selectOffer_1.selectOffer)({
|
|
88
|
+
scan: scanForReconcile,
|
|
89
|
+
history: reconciled,
|
|
90
|
+
now,
|
|
91
|
+
paintMuted: (s) => opts.display.paint(s, 'muted'),
|
|
92
|
+
paintAccent: (s) => theme_1.c.accent(s),
|
|
93
|
+
openItem: distillation?.openItem,
|
|
94
|
+
lastDecision: distillation?.lastDecision,
|
|
95
|
+
});
|
|
96
|
+
// ── Render (or stay silent) -----------------------------------------
|
|
97
|
+
if (offer) {
|
|
98
|
+
// 2-space indent + trailing blank to match firstRunHint layout.
|
|
99
|
+
opts.display.write(' ' + offer.speech + '\n\n');
|
|
100
|
+
}
|
|
101
|
+
// ── Persist updated history -----------------------------------------
|
|
102
|
+
const updated = {
|
|
103
|
+
...reconciled,
|
|
104
|
+
lastGreetingAt: now.toISOString(),
|
|
105
|
+
lastCwd: cwd,
|
|
106
|
+
offers: offer
|
|
107
|
+
? [...reconciled.offers, {
|
|
108
|
+
id: offer.id,
|
|
109
|
+
offeredAt: now.toISOString(),
|
|
110
|
+
expectedAction: offer.expectedAction,
|
|
111
|
+
}]
|
|
112
|
+
: reconciled.offers,
|
|
113
|
+
};
|
|
114
|
+
await (0, history_1.writeHistory)(opts.paths, updated, fsImpl);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read the newest distillation file and extract (open_items[0],
|
|
118
|
+
* decisions[0]). Returns null when no distillations exist or any IO
|
|
119
|
+
* fails — caller (selectOffer) treats null as "no continuity signal".
|
|
120
|
+
*
|
|
121
|
+
* Slice 1 strategy: list distillationsDir, sort by filename desc (the
|
|
122
|
+
* existing distillation naming convention timestamps the filename so
|
|
123
|
+
* lexicographic sort is reverse-chronological), read the newest, parse,
|
|
124
|
+
* extract. No schema dependency on the distillation index — just the
|
|
125
|
+
* field shape.
|
|
126
|
+
*/
|
|
127
|
+
async function loadLatestDistillation(paths, fsImpl) {
|
|
128
|
+
try {
|
|
129
|
+
const dir = node_path_1.default.join(paths.root, 'distillations');
|
|
130
|
+
const entries = await fsImpl.readdir(dir);
|
|
131
|
+
if (entries.length === 0)
|
|
132
|
+
return null;
|
|
133
|
+
const newest = [...entries].sort().reverse()[0];
|
|
134
|
+
const raw = await fsImpl.readFile(node_path_1.default.join(dir, newest), 'utf8');
|
|
135
|
+
const parsed = JSON.parse(raw);
|
|
136
|
+
const openItem = Array.isArray(parsed.open_items) && typeof parsed.open_items[0] === 'string'
|
|
137
|
+
? parsed.open_items[0]
|
|
138
|
+
: null;
|
|
139
|
+
const lastDecision = Array.isArray(parsed.decisions) && typeof parsed.decisions[0] === 'string'
|
|
140
|
+
? parsed.decisions[0]
|
|
141
|
+
: null;
|
|
142
|
+
return { openItem, lastDecision };
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
* cli/v4/greeter/scan.ts — v4.9.3 SLICE 1a.
|
|
10
|
+
*
|
|
11
|
+
* Four scanners feed `ScanResult`. None spawn subprocesses, none hit
|
|
12
|
+
* the network, none scan large files. Total cost target: < 20ms on
|
|
13
|
+
* a warm cache, < 50ms cold.
|
|
14
|
+
*
|
|
15
|
+
* • scanTimeOfDay — local hour from `now` (cheapest; no IO)
|
|
16
|
+
* • scanCwd — cwd vs history.lastCwd (no IO)
|
|
17
|
+
* • scanLastSessionEnd — mtime of newest distillation file
|
|
18
|
+
* • scanUpdate — reads existing `.update_check.json` cache
|
|
19
|
+
* (populated by core/v4/update/checkUpdate.ts);
|
|
20
|
+
* DOES NOT hit the npm registry itself —
|
|
21
|
+
* consumes the existing background check.
|
|
22
|
+
*
|
|
23
|
+
* Git observations are deferred to v4.10 per Phase A decision.
|
|
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.runScans = runScans;
|
|
30
|
+
exports.scanCwd = scanCwd;
|
|
31
|
+
exports.scanLastSessionEnd = scanLastSessionEnd;
|
|
32
|
+
exports.scanUpdate = scanUpdate;
|
|
33
|
+
const node_fs_1 = require("node:fs");
|
|
34
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
35
|
+
/**
|
|
36
|
+
* Run all four scanners and aggregate. Pure with respect to its
|
|
37
|
+
* `now` / `cwd` / `version` parameters — given identical inputs and
|
|
38
|
+
* identical disk state, produces identical output. The orchestrator
|
|
39
|
+
* supplies these explicitly so tests can drive them deterministically.
|
|
40
|
+
*/
|
|
41
|
+
async function runScans(input) {
|
|
42
|
+
const fsImpl = input.fsImpl ?? node_fs_1.promises;
|
|
43
|
+
const [hoursSinceLastSession, update] = await Promise.all([
|
|
44
|
+
scanLastSessionEnd(input.paths, input.now, fsImpl),
|
|
45
|
+
scanUpdate(input.paths, input.version, fsImpl),
|
|
46
|
+
]);
|
|
47
|
+
return {
|
|
48
|
+
hourOfDay: input.now.getHours(),
|
|
49
|
+
cwdChanged: scanCwd(input.cwd, input.history),
|
|
50
|
+
cwd: input.cwd,
|
|
51
|
+
hoursSinceLastSession,
|
|
52
|
+
update,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// ── Individual scanners — exported for fine-grained unit tests --------
|
|
56
|
+
/** True iff cwd differs from history.lastCwd. False when history has no
|
|
57
|
+
* prior cwd (treats first-seen-cwd as "not changed"). */
|
|
58
|
+
function scanCwd(cwd, history) {
|
|
59
|
+
if (!history.lastCwd)
|
|
60
|
+
return false;
|
|
61
|
+
return node_path_1.default.resolve(history.lastCwd) !== node_path_1.default.resolve(cwd);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Hours since the most recent distillation file (mtime). Returns null
|
|
65
|
+
* when the distillations directory is missing or empty — caller treats
|
|
66
|
+
* null as "no prior session to remember".
|
|
67
|
+
*
|
|
68
|
+
* Reads directory entries, takes the newest mtime, returns elapsed
|
|
69
|
+
* hours rounded to nearest int. Hard-caps at 100 entries scanned —
|
|
70
|
+
* if the user has thousands of distillations the cost stays bounded
|
|
71
|
+
* (we only care about the newest; sorting is fine on 100 entries).
|
|
72
|
+
*/
|
|
73
|
+
async function scanLastSessionEnd(paths, now, fsImpl = node_fs_1.promises) {
|
|
74
|
+
const dir = node_path_1.default.join(paths.root, 'distillations');
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
entries = await fsImpl.readdir(dir);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null; // dir missing → no prior session
|
|
81
|
+
}
|
|
82
|
+
if (entries.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
// Cap at 100 — newest-mtime extraction; we only need the max.
|
|
85
|
+
const scanList = entries.slice(0, 100);
|
|
86
|
+
let newest = 0;
|
|
87
|
+
for (const e of scanList) {
|
|
88
|
+
try {
|
|
89
|
+
const st = await fsImpl.stat(node_path_1.default.join(dir, e));
|
|
90
|
+
if (st.mtimeMs > newest)
|
|
91
|
+
newest = st.mtimeMs;
|
|
92
|
+
}
|
|
93
|
+
catch { /* skip unreadable entry */ }
|
|
94
|
+
}
|
|
95
|
+
if (newest === 0)
|
|
96
|
+
return null;
|
|
97
|
+
const elapsedMs = now.getTime() - newest;
|
|
98
|
+
return Math.max(0, Math.round(elapsedMs / (1000 * 60 * 60)));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Read the existing update-status cache (written by the background
|
|
102
|
+
* checkUpdate flow). Returns the update info when `latest > installed`,
|
|
103
|
+
* null otherwise. NEVER hits the network — the greeter consumes
|
|
104
|
+
* whatever the boot-time update-check already cached.
|
|
105
|
+
*
|
|
106
|
+
* Cache shape (per core/v4/update/checkUpdate.ts contract):
|
|
107
|
+
* { latest: string, lastCheckedAt: string, ... }
|
|
108
|
+
* We read minimally — just `latest`. If parsing fails, return null
|
|
109
|
+
* (don't speculate about an update we can't confirm).
|
|
110
|
+
*/
|
|
111
|
+
async function scanUpdate(paths, version, fsImpl = node_fs_1.promises) {
|
|
112
|
+
const cachePath = node_path_1.default.join(paths.root, '.update_check.json');
|
|
113
|
+
try {
|
|
114
|
+
const raw = await fsImpl.readFile(cachePath, 'utf8');
|
|
115
|
+
const parsed = JSON.parse(raw);
|
|
116
|
+
if (!parsed.latest || typeof parsed.latest !== 'string')
|
|
117
|
+
return null;
|
|
118
|
+
if (!isNewer(parsed.latest, version))
|
|
119
|
+
return null;
|
|
120
|
+
return { latest: parsed.latest, installed: version };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Returns true iff `a > b` under dot-numeric semver. Local copy so
|
|
127
|
+
* scan has no dependency on history's identical helper. */
|
|
128
|
+
function isNewer(a, b) {
|
|
129
|
+
const pa = a.split('.').map((s) => Number(s) || 0);
|
|
130
|
+
const pb = b.split('.').map((s) => Number(s) || 0);
|
|
131
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
132
|
+
const va = pa[i] ?? 0;
|
|
133
|
+
const vb = pb[i] ?? 0;
|
|
134
|
+
if (va > vb)
|
|
135
|
+
return true;
|
|
136
|
+
if (va < vb)
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
* cli/v4/greeter/selectOffer.ts — v4.9.3 SLICE 1a.
|
|
10
|
+
*
|
|
11
|
+
* Pure-function priority selector. Given the post-reconcile scan +
|
|
12
|
+
* history + (optional) distillation snippet, returns at most one
|
|
13
|
+
* `Offer` to render. Returns null when nothing wins (silence rule).
|
|
14
|
+
*
|
|
15
|
+
* Tier ordering: 1 > 2 > 3 > 4. Within a tier, the first detected
|
|
16
|
+
* candidate wins (no scoring beyond the order listed below).
|
|
17
|
+
*
|
|
18
|
+
* Decay (applied per tier): an offer whose `id` exists in history.offers
|
|
19
|
+
* with response === 'ignored' AND whose offeredAt is newer than the
|
|
20
|
+
* per-tier window is SUPPRESSED. Exception: welcome-back has no decay —
|
|
21
|
+
* it always fires when the threshold is crossed.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.selectOffer = selectOffer;
|
|
25
|
+
const types_1 = require("./types");
|
|
26
|
+
const templates_1 = require("./templates");
|
|
27
|
+
function selectOffer(input) {
|
|
28
|
+
// Greeter respects the kill switch absolutely.
|
|
29
|
+
if (input.history.disabled)
|
|
30
|
+
return null;
|
|
31
|
+
const today = isoDateLocal(input.now);
|
|
32
|
+
// ── Tier 2: continuity ----------------------------------------------
|
|
33
|
+
// The orchestrator wires open_items + decisions from the most-recent
|
|
34
|
+
// distillation. Prefer open-item over decision (open work is more
|
|
35
|
+
// actionable; closed decisions are recap).
|
|
36
|
+
if (input.openItem && input.openItem.length > 0) {
|
|
37
|
+
return buildOffer('continuity-open-item', 2, undefined, {
|
|
38
|
+
openItem: input.openItem,
|
|
39
|
+
}, input);
|
|
40
|
+
}
|
|
41
|
+
if (input.lastDecision && input.lastDecision.length > 0) {
|
|
42
|
+
return buildOffer('continuity-decision', 2, undefined, {
|
|
43
|
+
decision: input.lastDecision,
|
|
44
|
+
}, input);
|
|
45
|
+
}
|
|
46
|
+
// welcome-back: always fires when hoursSinceLastSession >= 24, no
|
|
47
|
+
// decay. (Per dispatch: not really an offer — a continuity signal.)
|
|
48
|
+
if (input.scan.hoursSinceLastSession !== null &&
|
|
49
|
+
input.scan.hoursSinceLastSession >= types_1.WELCOME_BACK_THRESHOLD_HOURS) {
|
|
50
|
+
return buildOffer('welcome-back', 2, undefined, {
|
|
51
|
+
hoursAgo: input.scan.hoursSinceLastSession,
|
|
52
|
+
}, input);
|
|
53
|
+
}
|
|
54
|
+
// ── Tier 3: environment ---------------------------------------------
|
|
55
|
+
// Both gated on no-tier-2-fired (handled implicitly by being later in
|
|
56
|
+
// the function) AND not-in-3-day-decay-window.
|
|
57
|
+
if (input.scan.hourOfDay >= 18) {
|
|
58
|
+
const id = `time-of-day-evening-${today}`;
|
|
59
|
+
if (!isDecayedRecently(id, input.history, types_1.DECAY_DAYS_ENVIRONMENT, input.now)) {
|
|
60
|
+
return buildOffer('time-of-day-evening', 3, undefined, {}, input, id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (input.scan.cwdChanged) {
|
|
64
|
+
const id = `cwd-changed-${today}`;
|
|
65
|
+
if (!isDecayedRecently(id, input.history, types_1.DECAY_DAYS_ENVIRONMENT, input.now)) {
|
|
66
|
+
return buildOffer('cwd-changed', 3, undefined, {
|
|
67
|
+
cwd: input.scan.cwd,
|
|
68
|
+
previousCwd: input.history.lastCwd,
|
|
69
|
+
}, input, id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ── Tier 4: update --------------------------------------------------
|
|
73
|
+
if (input.scan.update) {
|
|
74
|
+
const id = `update-available-${input.scan.update.latest}`;
|
|
75
|
+
if (!isDecayedRecently(id, input.history, types_1.DECAY_DAYS_UPDATE, input.now)) {
|
|
76
|
+
return buildOffer('update-available', 4, '/update install', {
|
|
77
|
+
installed: input.scan.update.installed,
|
|
78
|
+
latest: input.scan.update.latest,
|
|
79
|
+
}, input, id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null; // silence rule
|
|
83
|
+
}
|
|
84
|
+
// ── helpers ----------------------------------------------------------
|
|
85
|
+
/**
|
|
86
|
+
* True iff history contains an `ignored` record for `id` whose age is
|
|
87
|
+
* within the decay window. Pending offers do NOT suppress — only
|
|
88
|
+
* ignored ones do (caller has logic for re-firing if the user just
|
|
89
|
+
* didn't see it).
|
|
90
|
+
*/
|
|
91
|
+
function isDecayedRecently(id, history, days, now) {
|
|
92
|
+
const cutoffMs = now.getTime() - days * 24 * 60 * 60 * 1000;
|
|
93
|
+
return history.offers.some((o) => o.id === id &&
|
|
94
|
+
o.response === 'ignored' &&
|
|
95
|
+
Date.parse(o.offeredAt) >= cutoffMs);
|
|
96
|
+
}
|
|
97
|
+
/** YYYY-MM-DD in the local timezone (matches the "good evening at 6pm
|
|
98
|
+
* local time" intent of the time-of-day scanner). */
|
|
99
|
+
function isoDateLocal(d) {
|
|
100
|
+
const y = d.getFullYear();
|
|
101
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
102
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
103
|
+
return `${y}-${m}-${dd}`;
|
|
104
|
+
}
|
|
105
|
+
function buildOffer(templateId, tier, expectedAction, data, input, customId) {
|
|
106
|
+
const ctx = {
|
|
107
|
+
...data,
|
|
108
|
+
paintMuted: input.paintMuted,
|
|
109
|
+
paintAccent: input.paintAccent,
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
id: customId ?? `${templateId}-${isoDateLocal(input.now)}`,
|
|
113
|
+
templateId,
|
|
114
|
+
tier,
|
|
115
|
+
expectedAction,
|
|
116
|
+
speech: templates_1.TEMPLATES[templateId](ctx),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
* cli/v4/greeter/templates.ts — v4.9.3 SLICE 1a.
|
|
10
|
+
*
|
|
11
|
+
* Pure-function templates per TemplateId. Identical ctx ⇒ identical
|
|
12
|
+
* string out. No clock peek, no randomness, no env reads inside —
|
|
13
|
+
* every dynamic value arrives via the TemplateContext bag, including
|
|
14
|
+
* the two paint helpers.
|
|
15
|
+
*
|
|
16
|
+
* Render-site indent (2 spaces) and trailing newline are added by the
|
|
17
|
+
* orchestrator, NOT by these templates. Templates return one logical
|
|
18
|
+
* line of speech.
|
|
19
|
+
*/
|
|
20
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
21
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
22
|
+
};
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.TEMPLATES = void 0;
|
|
25
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
26
|
+
/**
|
|
27
|
+
* The eight templates. Tier-1 entries (daemon-crashed, hook-auto-disabled)
|
|
28
|
+
* are forward declarations — Slice 1's selectOffer never picks them. They
|
|
29
|
+
* exist so v4.10's tier-1 scanners have a typed home to drop offers into.
|
|
30
|
+
*/
|
|
31
|
+
exports.TEMPLATES = {
|
|
32
|
+
// ── Tier 1 (stubs — scanners deferred to v4.10) ----------------------
|
|
33
|
+
'daemon-crashed': (ctx) => `Daemon crashed mid-session. ${ctx.paintAccent('/daemon doctor')} for the postmortem.`,
|
|
34
|
+
'hook-auto-disabled': (ctx) => `A hook auto-disabled after repeated failures. ${ctx.paintAccent('/hooks audit')} for details.`,
|
|
35
|
+
// ── Tier 2 (continuity) ----------------------------------------------
|
|
36
|
+
'continuity-open-item': (ctx) => `Last session left this open: ${ctx.paintMuted(`"${ctx.openItem ?? ''}"`)}.`,
|
|
37
|
+
'continuity-decision': (ctx) => `Last session: ${ctx.paintMuted(ctx.decision ?? '')}.`,
|
|
38
|
+
'welcome-back': (ctx) => `Welcome back. Last session ended ${ctx.hoursAgo ?? 0}h ago.`,
|
|
39
|
+
// ── Tier 3 (environment) ---------------------------------------------
|
|
40
|
+
'time-of-day-evening': (_ctx) => `Good evening.`,
|
|
41
|
+
'cwd-changed': (ctx) => {
|
|
42
|
+
// Per user's prose suggestion: avoid "now" (implies temporal change).
|
|
43
|
+
// Phrasing: "In <basename> this time (last session: <previous>)."
|
|
44
|
+
const cur = ctx.cwd ? node_path_1.default.basename(ctx.cwd) : '';
|
|
45
|
+
const prv = ctx.previousCwd ? node_path_1.default.basename(ctx.previousCwd) : '';
|
|
46
|
+
return `In ${ctx.paintAccent(cur)} this time (last session: ${ctx.paintMuted(prv)}).`;
|
|
47
|
+
},
|
|
48
|
+
// ── Tier 4 (update) --------------------------------------------------
|
|
49
|
+
'update-available': (ctx) => `aiden-runtime ${ctx.installed ?? '?'} → ${ctx.latest ?? '?'} available. ` +
|
|
50
|
+
`${ctx.paintAccent('/update install')} to ship.`,
|
|
51
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
* cli/v4/greeter/types.ts — v4.9.3 SLICE 1a.
|
|
10
|
+
*
|
|
11
|
+
* Shared types for the boot greeter. Kept in one file so the rest of
|
|
12
|
+
* the module imports a single typed surface — no circular-import risk
|
|
13
|
+
* when scan / history / selectOffer / templates reference each other.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.WELCOME_BACK_THRESHOLD_HOURS = exports.DECAY_DAYS_ENVIRONMENT = exports.DECAY_DAYS_UPDATE = void 0;
|
|
17
|
+
// ── Decay windows -------------------------------------------------------
|
|
18
|
+
/** Days an "ignored" update offer remains suppressed. */
|
|
19
|
+
exports.DECAY_DAYS_UPDATE = 7;
|
|
20
|
+
/** Days an "ignored" environment offer (cwd, time-of-day) remains suppressed. */
|
|
21
|
+
exports.DECAY_DAYS_ENVIRONMENT = 3;
|
|
22
|
+
/** Hours since last session before welcome-back fires. */
|
|
23
|
+
exports.WELCOME_BACK_THRESHOLD_HOURS = 24;
|