agent-dag 1.33.90 → 1.33.92
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/dist/web/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>agents-deck</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='84' font-size='84'%3E%E2%97%89%3C/text%3E%3C/svg%3E" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CJhj8oVC.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/assets/index-dwRx-2y0.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-dag",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.92",
|
|
4
4
|
"description": "Live deck of Claude Code and Codex agents — watch parallel subagents fork, call tools, and return on one calm canvas. Also available as npx ccdeck and npx agent-dag.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Only ever touches its own entry, tagged `__agent-dag-sound`. Hooks the user
|
|
11
11
|
// wrote themselves are left exactly as found — including the platform-specific
|
|
12
12
|
// ones this replaces, which are reported rather than deleted.
|
|
13
|
-
import { readFile,
|
|
13
|
+
import { readFile, mkdir } from "node:fs/promises";
|
|
14
14
|
import { existsSync } from "node:fs";
|
|
15
15
|
import { join, dirname } from "node:path";
|
|
16
16
|
import { homedir } from "node:os";
|
|
@@ -60,6 +60,18 @@ function refusal(err) {
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const isParkFailure = (err) => err?.code === "PARKED_UNREADABLE" || err?.code === "PARKED_UNWRITABLE";
|
|
64
|
+
|
|
65
|
+
/** The same refusal, for the file the parked hooks live in rather than settings.json. */
|
|
66
|
+
function parkRefusal(err) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
reason: err?.code === "PARKED_UNREADABLE" ? "parked_unreadable" : "parked_unwritable",
|
|
70
|
+
parkedPath: PARKED_PATH,
|
|
71
|
+
message: err?.message ?? String(err),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
/**
|
|
64
76
|
* Write settings.json back atomically — this file holds every hook the user
|
|
65
77
|
* has, and a torn write costs them all of them.
|
|
@@ -91,18 +103,63 @@ function foreignSoundHooks(settings) {
|
|
|
91
103
|
return found;
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
function parkedError(code, why) {
|
|
107
|
+
const err = new Error(
|
|
108
|
+
code === "PARKED_UNREADABLE"
|
|
109
|
+
? `${PARKED_PATH} could not be read as JSON (${why}). It holds sound hooks you wrote yourself, ` +
|
|
110
|
+
`so it is not being treated as empty — fix the file or move it aside, then try again.`
|
|
111
|
+
: `${PARKED_PATH} could not be written (${why}). It is where your own sound hooks are kept while ` +
|
|
112
|
+
`the toggle is on, so nothing was taken out of settings.json — they would have been in neither file.`,
|
|
113
|
+
);
|
|
114
|
+
err.code = code;
|
|
115
|
+
err.parkedPath = PARKED_PATH;
|
|
116
|
+
return err;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read the parked hooks, refusing to guess at a file that will not parse.
|
|
121
|
+
*
|
|
122
|
+
* Same bargain as readSettingsForWrite, for the same reason: this is the only
|
|
123
|
+
* copy of hooks the user wrote by hand, and every caller either overwrites the
|
|
124
|
+
* file or reports how much is in it. A truncated file — a kill mid-write, a full
|
|
125
|
+
* disk — used to read as "nothing was ever parked", and the next toggle wrote
|
|
126
|
+
* its own list over the remains. Only ENOENT is genuinely empty; the array is
|
|
127
|
+
* not optional, because a JSON object here means the file is not ours to touch.
|
|
128
|
+
*/
|
|
94
129
|
async function readParked() {
|
|
130
|
+
let raw;
|
|
95
131
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
132
|
+
raw = await readFile(PARKED_PATH, "utf8");
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err?.code === "ENOENT") return [];
|
|
135
|
+
throw parkedError("PARKED_UNREADABLE", err?.message ?? String(err));
|
|
136
|
+
}
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(raw);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
throw parkedError("PARKED_UNREADABLE", err?.message ?? String(err));
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(parsed)) throw parkedError("PARKED_UNREADABLE", "top level is not a JSON array");
|
|
144
|
+
return parsed;
|
|
99
145
|
}
|
|
100
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Write the parked hooks, and never quietly fail to.
|
|
149
|
+
*
|
|
150
|
+
* This used to swallow every error, which made the park a suggestion: a
|
|
151
|
+
* root-owned ~/.agents-deck, a full disk or a Windows lock on the file left the
|
|
152
|
+
* write undone while setSoundHook went on to strip the same hooks out of
|
|
153
|
+
* settings.json and report success. Atomic for the other half of it — a torn
|
|
154
|
+
* parked file is a parked file that reads as empty.
|
|
155
|
+
*/
|
|
101
156
|
async function writeParked(entries) {
|
|
102
157
|
try {
|
|
103
158
|
if (!existsSync(dirname(PARKED_PATH))) await mkdir(dirname(PARKED_PATH), { recursive: true });
|
|
104
|
-
await
|
|
105
|
-
} catch {
|
|
159
|
+
await writeFileAtomic(PARKED_PATH, JSON.stringify(entries, null, 2) + "\n");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
throw parkedError("PARKED_UNWRITABLE", err?.message ?? String(err));
|
|
162
|
+
}
|
|
106
163
|
}
|
|
107
164
|
|
|
108
165
|
/**
|
|
@@ -128,13 +185,30 @@ export async function soundHookStatus() {
|
|
|
128
185
|
// Reporting a healthy "off" here would be a lie the user acts on: the
|
|
129
186
|
// toggle cannot do anything until they repair the file, so say which file
|
|
130
187
|
// and why rather than offering a switch that will refuse.
|
|
131
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
...refusal(err),
|
|
190
|
+
enabled: false,
|
|
191
|
+
platform: process.platform,
|
|
192
|
+
foreign: [],
|
|
193
|
+
// A parked file that will not read is a second refusal, and settings.json
|
|
194
|
+
// is the one being reported. Count what can be counted.
|
|
195
|
+
parked: await readParked().then(p => p.length, () => 0),
|
|
196
|
+
};
|
|
132
197
|
}
|
|
133
198
|
const group = settings?.hooks?.[EVENT];
|
|
134
|
-
const
|
|
199
|
+
const enabled = Array.isArray(group) && group.some(isOurs);
|
|
200
|
+
let parked;
|
|
201
|
+
try {
|
|
202
|
+
parked = await readParked();
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (!isParkFailure(err)) throw err;
|
|
205
|
+
// "parked: 0" on a file we cannot read is the lie that sends the user to
|
|
206
|
+
// click the toggle, which is the thing that would overwrite it.
|
|
207
|
+
return { ...parkRefusal(err), enabled, platform: process.platform, foreign: foreignSoundHooks(settings), parked: 0 };
|
|
208
|
+
}
|
|
135
209
|
return {
|
|
136
210
|
ok: true,
|
|
137
|
-
enabled
|
|
211
|
+
enabled,
|
|
138
212
|
platform: process.platform,
|
|
139
213
|
foreign: foreignSoundHooks(settings),
|
|
140
214
|
parked: parked.length,
|
|
@@ -148,7 +222,15 @@ export async function soundHookStatus() {
|
|
|
148
222
|
* can have it back exactly as it was.
|
|
149
223
|
*/
|
|
150
224
|
export async function restoreParkedSoundHooks() {
|
|
151
|
-
|
|
225
|
+
let parked;
|
|
226
|
+
try {
|
|
227
|
+
parked = await readParked();
|
|
228
|
+
} catch (err) {
|
|
229
|
+
if (!isParkFailure(err)) throw err;
|
|
230
|
+
// The file is left exactly as it is. Answering "restored: 0" would be the
|
|
231
|
+
// last word on hooks that are still in there, badly written but present.
|
|
232
|
+
return parkRefusal(err);
|
|
233
|
+
}
|
|
152
234
|
if (parked.length === 0) return { ok: true, restored: 0 };
|
|
153
235
|
let settings;
|
|
154
236
|
try {
|
|
@@ -163,7 +245,15 @@ export async function restoreParkedSoundHooks() {
|
|
|
163
245
|
const group = Array.isArray(settings.hooks[EVENT]) ? settings.hooks[EVENT] : [];
|
|
164
246
|
settings.hooks[EVENT] = [...parked, ...group];
|
|
165
247
|
await writeSettings(settings);
|
|
166
|
-
|
|
248
|
+
// Emptying the park comes last and its failure is reported, not swallowed:
|
|
249
|
+
// the hooks are safely back in settings.json now, but a park left behind is
|
|
250
|
+
// one the next restore hands over a second time, duplicating them.
|
|
251
|
+
try {
|
|
252
|
+
await writeParked([]);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (!isParkFailure(err)) throw err;
|
|
255
|
+
return { ...parkRefusal(err), restored: parked.length };
|
|
256
|
+
}
|
|
167
257
|
return { ok: true, restored: parked.length };
|
|
168
258
|
}
|
|
169
259
|
|
|
@@ -221,9 +311,19 @@ export async function setSoundHook(enabled) {
|
|
|
221
311
|
// Without this the toggle is a lie in both directions: off still plays their
|
|
222
312
|
// afplay/PowerShell hook, and on plays twice. They are moved, not deleted —
|
|
223
313
|
// restoreParkedSoundHooks puts them back untouched.
|
|
314
|
+
//
|
|
315
|
+
// Which only holds if the move lands first. The filter below drops exactly
|
|
316
|
+
// these entries from the object written to settings.json, so a park that
|
|
317
|
+
// failed and said nothing left the user's own Stop hook in neither file, with
|
|
318
|
+
// ok:true on the way out. Nothing here is written until the park is on disk.
|
|
224
319
|
const parking = group.filter(isSoundHook);
|
|
225
320
|
if (parking.length > 0) {
|
|
226
|
-
|
|
321
|
+
try {
|
|
322
|
+
await writeParked([...(await readParked()), ...parking]);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (!isParkFailure(err)) throw err;
|
|
325
|
+
return parkRefusal(err);
|
|
326
|
+
}
|
|
227
327
|
}
|
|
228
328
|
const others = group.filter(g => !isOurs(g) && !isSoundHook(g));
|
|
229
329
|
|