@kitfunso/aura 0.1.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.
package/src/state.js ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ // Local session state: HWND cache + latest prompt, keyed by Claude session id.
3
+ // One small JSON file, rewritten atomically (temp + rename).
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+
8
+ const STALE_MS = 48 * 60 * 60 * 1000;
9
+ const LOCK_STALE_MS = 5000;
10
+ const LOCK_WAIT_MS = 200;
11
+ const LOCK_SLICE_MS = 5;
12
+ const RENAME_WAIT_MS = 250;
13
+ const RENAME_SLICE_MS = 5;
14
+
15
+ function stateDir() {
16
+ if (process.platform === "win32") {
17
+ const base = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
18
+ return path.join(base, "aura");
19
+ }
20
+ const base = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
21
+ return path.join(base, "aura");
22
+ }
23
+
24
+ function stateFile() {
25
+ return path.join(stateDir(), "state.json");
26
+ }
27
+
28
+ // null means "there is state here but it would not read", which is not the same
29
+ // as no state: writing an empty file back would delete every other window.
30
+ function readState() {
31
+ try {
32
+ const parsed = JSON.parse(fs.readFileSync(stateFile(), "utf8"));
33
+ if (parsed && typeof parsed === "object" && parsed.sessions) return parsed;
34
+ } catch (err) {
35
+ if (err.code && err.code !== "ENOENT") return null;
36
+ }
37
+ return { sessions: {} };
38
+ }
39
+
40
+ function writeState(state) {
41
+ const file = stateFile();
42
+ fs.mkdirSync(path.dirname(file), { recursive: true });
43
+ // Per-process temp name, so two writers can never share one.
44
+ const temp = file + "." + process.pid + ".tmp";
45
+ fs.writeFileSync(temp, JSON.stringify(state, null, 2));
46
+ // Windows refuses the rename while a reader holds the destination open, and
47
+ // allows it the moment that reader closes (measured), so wait the reader out.
48
+ const deadline = Date.now() + RENAME_WAIT_MS;
49
+ for (;;) {
50
+ try {
51
+ fs.renameSync(temp, file);
52
+ return;
53
+ } catch (err) {
54
+ if (Date.now() >= deadline) {
55
+ try { fs.unlinkSync(temp); } catch (cleanupErr) { /* nothing left to do */ }
56
+ throw err;
57
+ }
58
+ sleep(RENAME_SLICE_MS);
59
+ }
60
+ }
61
+ }
62
+
63
+ function pruneStale(state, now = Date.now()) {
64
+ for (const [id, session] of Object.entries(state.sessions)) {
65
+ const updated = Date.parse(session.updatedAt || "");
66
+ if (!Number.isFinite(updated) || now - updated > STALE_MS) {
67
+ delete state.sessions[id];
68
+ }
69
+ }
70
+ // A tag outlives nothing: its session is the only thing that gives it meaning.
71
+ for (const id of Object.keys(state.tags || {})) {
72
+ if (!state.sessions[id]) delete state.tags[id];
73
+ }
74
+ const trackedHwnds = new Set();
75
+ for (const session of Object.values(state.sessions)) {
76
+ if (session.hwnd) trackedHwnds.add(String(session.hwnd));
77
+ }
78
+ for (const hwnd of Object.keys(state.frameOwner || {})) {
79
+ if (!trackedHwnds.has(hwnd)) delete state.frameOwner[hwnd];
80
+ }
81
+ return state;
82
+ }
83
+
84
+ function sleep(ms) {
85
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
86
+ }
87
+
88
+ // Exclusive create is the atomic primitive. A lock older than the adapter
89
+ // timeout belongs to a process that died holding it, so it is taken.
90
+ function acquireLock() {
91
+ const lock = stateFile() + ".lock";
92
+ const deadline = Date.now() + LOCK_WAIT_MS;
93
+ fs.mkdirSync(path.dirname(lock), { recursive: true });
94
+ for (;;) {
95
+ try {
96
+ fs.closeSync(fs.openSync(lock, "wx"));
97
+ return function () { try { fs.unlinkSync(lock); } catch (err) { /* already gone */ } };
98
+ } catch (err) {
99
+ try {
100
+ if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_STALE_MS) fs.unlinkSync(lock);
101
+ } catch (staleErr) { /* another waiter took it first */ }
102
+ // Rule 6: never stall a prompt. Giving up writes NOTHING, because an
103
+ // unsynchronized write is the loss the lock exists to prevent.
104
+ if (Date.now() >= deadline) return null;
105
+ sleep(LOCK_SLICE_MS);
106
+ }
107
+ }
108
+ }
109
+
110
+ // The caller's snapshot can be seconds old, because the adapter spawn happens
111
+ // between the read and here, and every shell prompt is a competing writer.
112
+ function updateState(mutate) {
113
+ const release = acquireLock();
114
+ if (!release) return false;
115
+ try {
116
+ const state = readState();
117
+ if (!state) return false;
118
+ mutate(state);
119
+ pruneStale(state);
120
+ writeState(state);
121
+ return true;
122
+ } finally {
123
+ release();
124
+ }
125
+ }
126
+
127
+ module.exports = { readState, writeState, updateState, pruneStale, stateDir, stateFile };
package/src/tag.js ADDED
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ // A second identity source, for a session whose working directory says nothing
3
+ // useful: an agent launched from a home folder. Design: docs/ARCHITECTURE.md.
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { readState, updateState } = require("./state.js");
7
+
8
+ // Every agent that runs in a terminal exports one of these, so a tag set from
9
+ // inside a session lands on the same key its own prompts will write.
10
+ function sessionKey(env, explicit) {
11
+ return explicit || env.CLAUDE_CODE_SESSION_ID || env.AURA_SESSION || "shell-" + process.ppid;
12
+ }
13
+
14
+ // Proof that a person is sitting in a terminal. A headless run has none of
15
+ // these, and painting a foreground window there colors an unrelated app.
16
+ const SESSION_MARKERS = [
17
+ "WT_SESSION", "TERM_PROGRAM", "CLAUDE_CODE_SESSION_ID", "AURA_SESSION",
18
+ "WEZTERM_PANE", "ALACRITTY_WINDOW_ID", "GHOSTTY_RESOURCES_DIR",
19
+ ];
20
+
21
+ function inTerminalSession(env) {
22
+ return SESSION_MARKERS.some(function (name) { return Boolean(env[name]); });
23
+ }
24
+
25
+ function readTag(sessionId) {
26
+ const state = readState();
27
+ return (state && state.tags && state.tags[sessionId]) || null;
28
+ }
29
+
30
+ function writeTag(sessionId, target) {
31
+ return updateState(function (fresh) {
32
+ const tags = fresh.tags || (fresh.tags = {});
33
+ if (target) tags[sessionId] = target;
34
+ else delete tags[sessionId];
35
+ // Without an entry the prune below would drop the tag in the same write.
36
+ const session = fresh.sessions[sessionId] || (fresh.sessions[sessionId] = {});
37
+ session.updatedAt = new Date().toISOString();
38
+ });
39
+ }
40
+
41
+ function resolveTarget(arg, cwd) {
42
+ const target = path.resolve(cwd, arg);
43
+ try {
44
+ return fs.statSync(target).isDirectory() ? target : null;
45
+ } catch (err) {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ module.exports = { sessionKey, readTag, writeTag, resolveTarget, inTerminalSession };
package/src/tty.js ADDED
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ // Writes escape bytes to the LIVE terminal device, never stdout (rule 3:
3
+ // Claude Code captures hook stdout as model context).
4
+ const fs = require("fs");
5
+
6
+ function writeToTerminal(text) {
7
+ const targets = process.platform === "win32"
8
+ ? ["\\\\.\\CONOUT$", "CONOUT$"]
9
+ : ["/dev/tty"];
10
+ for (const target of targets) {
11
+ let fd = null;
12
+ try {
13
+ fd = fs.openSync(target, "w");
14
+ fs.writeSync(fd, text);
15
+ return target;
16
+ } catch (err) {
17
+ // fall through to the next device path
18
+ } finally {
19
+ if (fd !== null) {
20
+ try { fs.closeSync(fd); } catch (err) { /* already closed */ }
21
+ }
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+
27
+ module.exports = { writeToTerminal };