agent-andon 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/LICENSE +21 -0
- package/README.md +221 -0
- package/assets/dashboard.html +362 -0
- package/dist/assets.d.ts +20 -0
- package/dist/assets.js +30 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +99 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +92 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +107 -0
- package/dist/commands/hook.d.ts +7 -0
- package/dist/commands/hook.js +76 -0
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +146 -0
- package/dist/commands/notify.d.ts +7 -0
- package/dist/commands/notify.js +55 -0
- package/dist/commands/post.d.ts +1 -0
- package/dist/commands/post.js +44 -0
- package/dist/commands/serve.d.ts +8 -0
- package/dist/commands/serve.js +96 -0
- package/dist/commands/shared.d.ts +12 -0
- package/dist/commands/shared.js +60 -0
- package/dist/demo.d.ts +3 -0
- package/dist/demo.js +33 -0
- package/dist/net.d.ts +7 -0
- package/dist/net.js +64 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.js +181 -0
- package/dist/store.d.ts +30 -0
- package/dist/store.js +76 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +28 -0
- package/docs/board.png +0 -0
- package/examples/codex-wrapper.sh +21 -0
- package/examples/com.agentandon.server.plist +33 -0
- package/package.json +55 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* Agent Andon CLI — one binary, many verbs.
|
|
6
|
+
*
|
|
7
|
+
* andon serve [--demo] [--port N] [--token T] run the board
|
|
8
|
+
* andon hook Claude Code hook (stdin)
|
|
9
|
+
* andon notify <json> Codex notify (argv)
|
|
10
|
+
* andon post <state> <agent> [title] [message] manual / wrapper push
|
|
11
|
+
* andon install <claude|codex> [--dry-run] auto-wire the hooks
|
|
12
|
+
* andon doctor check what's wired
|
|
13
|
+
* andon help
|
|
14
|
+
*
|
|
15
|
+
* `hook` and `notify` MUST never block or crash the agent: they always exit 0.
|
|
16
|
+
*/
|
|
17
|
+
const serve_1 = require("./commands/serve");
|
|
18
|
+
const hook_1 = require("./commands/hook");
|
|
19
|
+
const notify_1 = require("./commands/notify");
|
|
20
|
+
const post_1 = require("./commands/post");
|
|
21
|
+
const install_1 = require("./commands/install");
|
|
22
|
+
const doctor_1 = require("./commands/doctor");
|
|
23
|
+
const HELP = `
|
|
24
|
+
🚦 Agent Andon — a traffic-light board for your AI coding agents
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
andon serve [--demo] [--port N] [--host H] [--token T]
|
|
28
|
+
Run the status board. Open the printed URL on your iPad.
|
|
29
|
+
--demo inject fake agents so you can verify the board first.
|
|
30
|
+
|
|
31
|
+
andon install claude Wire Claude Code hooks (backs up settings.json)
|
|
32
|
+
andon install codex Wire the Codex notify hook
|
|
33
|
+
andon doctor Check server + what's wired, print the iPad URL
|
|
34
|
+
|
|
35
|
+
andon post <state> <agent> [title] [message]
|
|
36
|
+
Push a status by hand. state: working|waiting|done|error|idle|gone
|
|
37
|
+
e.g. andon post done claude "api" "shipped it"
|
|
38
|
+
|
|
39
|
+
andon hook (internal) Claude Code hook — reads stdin
|
|
40
|
+
andon notify <json> (internal) Codex notifier — reads argv
|
|
41
|
+
|
|
42
|
+
Env:
|
|
43
|
+
AGENT_STATUS_URL server base (default http://127.0.0.1:8787)
|
|
44
|
+
ANDON_TOKEN shared token; required by /state and /event when set
|
|
45
|
+
ANDON_LABEL per-terminal tile title
|
|
46
|
+
ANDON_SESSION per-launch session id (set by the codex wrapper)
|
|
47
|
+
|
|
48
|
+
Quickstart:
|
|
49
|
+
andon serve --demo # verify on the iPad, then Ctrl-C
|
|
50
|
+
andon serve # run for real
|
|
51
|
+
andon install claude # wire it up, restart your Claude session
|
|
52
|
+
`;
|
|
53
|
+
async function main() {
|
|
54
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
55
|
+
switch (cmd) {
|
|
56
|
+
case "serve":
|
|
57
|
+
(0, serve_1.serve)(rest);
|
|
58
|
+
return; // long-running; never returns
|
|
59
|
+
case "hook":
|
|
60
|
+
// Always exit 0 no matter what — see module docstring.
|
|
61
|
+
try {
|
|
62
|
+
await (0, hook_1.hook)();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* swallow */
|
|
66
|
+
}
|
|
67
|
+
process.exit(0);
|
|
68
|
+
return;
|
|
69
|
+
case "notify":
|
|
70
|
+
try {
|
|
71
|
+
await (0, notify_1.notify)(rest[0]);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
/* swallow */
|
|
75
|
+
}
|
|
76
|
+
process.exit(0);
|
|
77
|
+
return;
|
|
78
|
+
case "post":
|
|
79
|
+
process.exit(await (0, post_1.post)(rest));
|
|
80
|
+
return;
|
|
81
|
+
case "install":
|
|
82
|
+
process.exit((0, install_1.install)(rest));
|
|
83
|
+
return;
|
|
84
|
+
case "doctor":
|
|
85
|
+
process.exit(await (0, doctor_1.doctor)());
|
|
86
|
+
return;
|
|
87
|
+
case "help":
|
|
88
|
+
case "--help":
|
|
89
|
+
case "-h":
|
|
90
|
+
case undefined:
|
|
91
|
+
console.log(HELP);
|
|
92
|
+
process.exit(cmd ? 0 : 1);
|
|
93
|
+
return;
|
|
94
|
+
default:
|
|
95
|
+
console.error(`unknown command: ${cmd}\n${HELP}`);
|
|
96
|
+
process.exit(2);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
void main();
|
package/dist/client.d.ts
ADDED
package/dist/client.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.postEvent = postEvent;
|
|
37
|
+
/**
|
|
38
|
+
* The one and only status poster. Hooks, the Codex notifier and the manual
|
|
39
|
+
* `post` command all funnel through here — no more copy-pasted request code.
|
|
40
|
+
*
|
|
41
|
+
* Contract for callers in a hook path: this NEVER throws and NEVER blocks for
|
|
42
|
+
* long. A missing server, refused connection or timeout all resolve to `false`
|
|
43
|
+
* so the calling hook can exit 0 without ever stalling the agent.
|
|
44
|
+
*/
|
|
45
|
+
const http = __importStar(require("http"));
|
|
46
|
+
const https = __importStar(require("https"));
|
|
47
|
+
const url_1 = require("url");
|
|
48
|
+
const net_1 = require("./net");
|
|
49
|
+
function postEvent(ev, timeoutMs = 1500) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
let settled = false;
|
|
52
|
+
const finish = (r) => {
|
|
53
|
+
if (!settled) {
|
|
54
|
+
settled = true;
|
|
55
|
+
resolve(r);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
const token = process.env.ANDON_TOKEN;
|
|
60
|
+
const u = new url_1.URL((0, net_1.serverBase)() + "/event");
|
|
61
|
+
const body = Buffer.from(JSON.stringify(ev), "utf8");
|
|
62
|
+
const lib = u.protocol === "https:" ? https : http;
|
|
63
|
+
const headers = {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"Content-Length": body.length,
|
|
66
|
+
};
|
|
67
|
+
// Send the token as a header, not a query param — keeps it out of URLs/logs.
|
|
68
|
+
if (token)
|
|
69
|
+
headers["x-andon-token"] = token;
|
|
70
|
+
const req = lib.request({
|
|
71
|
+
hostname: u.hostname,
|
|
72
|
+
port: u.port || (u.protocol === "https:" ? 443 : 80),
|
|
73
|
+
path: u.pathname + u.search,
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers,
|
|
76
|
+
}, (res) => {
|
|
77
|
+
res.resume(); // drain
|
|
78
|
+
res.on("end", () => finish({ ok: (res.statusCode ?? 0) < 400, status: res.statusCode }));
|
|
79
|
+
});
|
|
80
|
+
req.on("error", (e) => finish({ ok: false, error: String(e?.message ?? e) }));
|
|
81
|
+
req.setTimeout(timeoutMs, () => {
|
|
82
|
+
req.destroy();
|
|
83
|
+
finish({ ok: false, error: "timeout" });
|
|
84
|
+
});
|
|
85
|
+
req.write(body);
|
|
86
|
+
req.end();
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
finish({ ok: false, error: String(e?.message ?? e) });
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function doctor(): Promise<number>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.doctor = doctor;
|
|
37
|
+
/** `andon doctor` — quick "is everything wired?" check. */
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const http = __importStar(require("http"));
|
|
42
|
+
const net_1 = require("../net");
|
|
43
|
+
function getJson(url, timeoutMs = 1200) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const req = http.get(url, (res) => {
|
|
46
|
+
let body = "";
|
|
47
|
+
res.on("data", (c) => (body += c));
|
|
48
|
+
res.on("end", () => {
|
|
49
|
+
try {
|
|
50
|
+
resolve(JSON.parse(body));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
reject(e);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
req.on("error", reject);
|
|
58
|
+
req.setTimeout(timeoutMs, () => {
|
|
59
|
+
req.destroy();
|
|
60
|
+
reject(new Error("timeout"));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function doctor() {
|
|
65
|
+
const base = (0, net_1.serverBase)();
|
|
66
|
+
console.log("\n Agent Andon — doctor\n ─────────────────────");
|
|
67
|
+
// 1. server reachable?
|
|
68
|
+
let serverOk = false;
|
|
69
|
+
try {
|
|
70
|
+
const h = (await getJson(`${base}/healthz`));
|
|
71
|
+
serverOk = !!h.ok;
|
|
72
|
+
console.log(` ✓ server up at ${base} (${h.sessions ?? 0} session(s))`);
|
|
73
|
+
const m = base.match(/:(\d+)/);
|
|
74
|
+
const port = m ? m[1] : "8787";
|
|
75
|
+
console.log(` → iPad: http://${(0, net_1.lanIp)()}:${port}`);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
console.log(` ✗ server NOT reachable at ${base}`);
|
|
79
|
+
console.log(" start it with: andon serve");
|
|
80
|
+
}
|
|
81
|
+
// 2. claude wired?
|
|
82
|
+
const claudeCfg = path.join(os.homedir(), ".claude", "settings.json");
|
|
83
|
+
if (fs.existsSync(claudeCfg)) {
|
|
84
|
+
const txt = fs.readFileSync(claudeCfg, "utf8");
|
|
85
|
+
const wired = txt.includes("cli.js") && txt.includes("hook");
|
|
86
|
+
console.log(wired
|
|
87
|
+
? " ✓ Claude Code hooks wired"
|
|
88
|
+
: " ○ Claude Code not wired — run: andon install claude");
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
console.log(" ○ no ~/.claude/settings.json — run: andon install claude");
|
|
92
|
+
}
|
|
93
|
+
// 3. codex wired?
|
|
94
|
+
const codexCfg = path.join(os.homedir(), ".codex", "config.toml");
|
|
95
|
+
if (fs.existsSync(codexCfg)) {
|
|
96
|
+
const txt = fs.readFileSync(codexCfg, "utf8");
|
|
97
|
+
const wired = /notify\s*=.*cli\.js/.test(txt);
|
|
98
|
+
console.log(wired
|
|
99
|
+
? " ✓ Codex notify wired"
|
|
100
|
+
: " ○ Codex notify not wired — run: andon install codex");
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log(" ○ no ~/.codex/config.toml — run: andon install codex (if you use Codex)");
|
|
104
|
+
}
|
|
105
|
+
console.log("");
|
|
106
|
+
return serverOk ? 0 : 1;
|
|
107
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AndonEvent } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Pure mapping from a Claude Code hook payload to a board event (no I/O).
|
|
4
|
+
* Returns null for events we don't track. Exported for testing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mapClaudeEvent(data: Record<string, unknown>): AndonEvent | null;
|
|
7
|
+
export declare function hook(): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapClaudeEvent = mapClaudeEvent;
|
|
4
|
+
exports.hook = hook;
|
|
5
|
+
/**
|
|
6
|
+
* `andon hook` — the Claude Code hook. Reads the hook JSON on stdin, maps the
|
|
7
|
+
* event to a board state, posts it. Wired to 5 events (see `andon install`):
|
|
8
|
+
*
|
|
9
|
+
* UserPromptSubmit -> working you just submitted, the agent is off
|
|
10
|
+
* Notification -> waiting needs permission / your input
|
|
11
|
+
* Stop -> done this turn finished
|
|
12
|
+
* StopFailure -> error this turn failed (newer Claude Code only)
|
|
13
|
+
* SessionEnd -> gone session ended, drop the tile
|
|
14
|
+
*
|
|
15
|
+
* Discipline: print nothing to stdout (UserPromptSubmit stdout is fed to the
|
|
16
|
+
* model as context), swallow every error, and let the caller always exit 0.
|
|
17
|
+
*/
|
|
18
|
+
const client_1 = require("../client");
|
|
19
|
+
const shared_1 = require("./shared");
|
|
20
|
+
const EVENT_TO_STATE = {
|
|
21
|
+
UserPromptSubmit: "working",
|
|
22
|
+
Notification: "waiting",
|
|
23
|
+
Stop: "done",
|
|
24
|
+
StopFailure: "error",
|
|
25
|
+
SessionEnd: "gone",
|
|
26
|
+
};
|
|
27
|
+
function readStdin() {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
let data = "";
|
|
30
|
+
let resolved = false;
|
|
31
|
+
const done = () => {
|
|
32
|
+
if (!resolved) {
|
|
33
|
+
resolved = true;
|
|
34
|
+
resolve(data);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
if (process.stdin.isTTY)
|
|
38
|
+
return done(); // no piped input
|
|
39
|
+
process.stdin.setEncoding("utf8");
|
|
40
|
+
process.stdin.on("data", (c) => (data += c));
|
|
41
|
+
process.stdin.on("end", done);
|
|
42
|
+
process.stdin.on("error", done);
|
|
43
|
+
setTimeout(done, 800).unref(); // never hang the hook
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const shorten = (t, n = 140) => String(t ?? "").split(/\s+/).join(" ").trim().slice(0, n);
|
|
47
|
+
/**
|
|
48
|
+
* Pure mapping from a Claude Code hook payload to a board event (no I/O).
|
|
49
|
+
* Returns null for events we don't track. Exported for testing.
|
|
50
|
+
*/
|
|
51
|
+
function mapClaudeEvent(data) {
|
|
52
|
+
const evName = String(data.hook_event_name ?? "");
|
|
53
|
+
const state = EVENT_TO_STATE[evName];
|
|
54
|
+
if (!state)
|
|
55
|
+
return null; // unknown event: quietly do nothing
|
|
56
|
+
const cwd = String(data.cwd ?? process.cwd());
|
|
57
|
+
const id = String(data.session_id ?? "claude");
|
|
58
|
+
let message = "";
|
|
59
|
+
if (evName === "Notification")
|
|
60
|
+
message = shorten(data.message);
|
|
61
|
+
else if (evName === "Stop" || evName === "StopFailure")
|
|
62
|
+
message = shorten(data.last_assistant_message);
|
|
63
|
+
return { agent: "claude", id, state, title: (0, shared_1.labelFor)(cwd, "claude"), message };
|
|
64
|
+
}
|
|
65
|
+
async function hook() {
|
|
66
|
+
let data = {};
|
|
67
|
+
try {
|
|
68
|
+
data = JSON.parse((await readStdin()) || "{}");
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
data = {};
|
|
72
|
+
}
|
|
73
|
+
const ev = mapClaudeEvent(data);
|
|
74
|
+
if (ev)
|
|
75
|
+
await (0, client_1.postEvent)(ev);
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function install(args: string[]): number;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.install = install;
|
|
37
|
+
/**
|
|
38
|
+
* `andon install claude|codex [--dry-run]`
|
|
39
|
+
*
|
|
40
|
+
* Auto-wires the hooks so nobody hand-edits config paths (the exact thing that
|
|
41
|
+
* trips people up). Always backs up the original file first.
|
|
42
|
+
*/
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const os = __importStar(require("os"));
|
|
45
|
+
const path = __importStar(require("path"));
|
|
46
|
+
/** Absolute `node /abs/dist/cli.js` invocation — works regardless of PATH. */
|
|
47
|
+
function andonCommand(sub) {
|
|
48
|
+
const cli = path.join(__dirname, "..", "cli.js");
|
|
49
|
+
return `"${process.execPath}" "${cli}" ${sub}`;
|
|
50
|
+
}
|
|
51
|
+
function backup(file) {
|
|
52
|
+
if (!fs.existsSync(file))
|
|
53
|
+
return null;
|
|
54
|
+
const bak = `${file}.andon-backup`;
|
|
55
|
+
fs.copyFileSync(file, bak);
|
|
56
|
+
return bak;
|
|
57
|
+
}
|
|
58
|
+
const CLAUDE_EVENTS = [
|
|
59
|
+
"UserPromptSubmit",
|
|
60
|
+
"Notification",
|
|
61
|
+
"Stop",
|
|
62
|
+
"StopFailure",
|
|
63
|
+
"SessionEnd",
|
|
64
|
+
];
|
|
65
|
+
function installClaude(dryRun) {
|
|
66
|
+
const file = path.join(os.homedir(), ".claude", "settings.json");
|
|
67
|
+
const cmd = andonCommand("hook");
|
|
68
|
+
let settings = {};
|
|
69
|
+
if (fs.existsSync(file)) {
|
|
70
|
+
try {
|
|
71
|
+
settings = JSON.parse(fs.readFileSync(file, "utf8") || "{}");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
console.error(`✗ ${file} is not valid JSON — fix or move it, then retry.`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const hooks = (settings.hooks ??= {});
|
|
79
|
+
let added = 0;
|
|
80
|
+
for (const ev of CLAUDE_EVENTS) {
|
|
81
|
+
const groups = (hooks[ev] ??= []);
|
|
82
|
+
const already = groups.some((g) => (g.hooks ?? []).some((h) => h.command?.includes("cli.js") && h.command?.includes("hook")));
|
|
83
|
+
if (!already) {
|
|
84
|
+
groups.push({ hooks: [{ type: "command", command: cmd }] });
|
|
85
|
+
added++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (dryRun) {
|
|
89
|
+
console.log(`[dry-run] would write ${file}:\n`);
|
|
90
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
if (added === 0) {
|
|
94
|
+
console.log("✓ Claude Code is already wired to Andon. Nothing to do.");
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
98
|
+
const bak = backup(file);
|
|
99
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n");
|
|
100
|
+
console.log(`✓ Wired ${added} Claude Code event(s) into ${file}`);
|
|
101
|
+
if (bak)
|
|
102
|
+
console.log(` backup: ${bak}`);
|
|
103
|
+
console.log("\n → Start a new Claude Code session and it lights up the board.");
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
function installCodex(dryRun) {
|
|
107
|
+
const file = path.join(os.homedir(), ".codex", "config.toml");
|
|
108
|
+
const cli = path.join(__dirname, "..", "cli.js");
|
|
109
|
+
const line = `notify = ["${process.execPath}", "${cli}", "notify"]`;
|
|
110
|
+
let body = "";
|
|
111
|
+
if (fs.existsSync(file))
|
|
112
|
+
body = fs.readFileSync(file, "utf8");
|
|
113
|
+
if (/^\s*notify\s*=/m.test(body)) {
|
|
114
|
+
console.log("ℹ A `notify = …` line already exists in ~/.codex/config.toml.");
|
|
115
|
+
console.log(" Leaving it untouched. To use Andon, set it to:");
|
|
116
|
+
console.log(` ${line}`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
// The notify key must sit ABOVE any [table] or TOML parses it into that table.
|
|
120
|
+
const next = `${line}\n${body.startsWith("\n") ? "" : "\n"}${body}`;
|
|
121
|
+
if (dryRun) {
|
|
122
|
+
console.log(`[dry-run] would prepend to ${file}:\n`);
|
|
123
|
+
console.log(line);
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
127
|
+
const bak = backup(file);
|
|
128
|
+
fs.writeFileSync(file, next);
|
|
129
|
+
console.log(`✓ Added Codex notify hook to ${file}`);
|
|
130
|
+
if (bak)
|
|
131
|
+
console.log(` backup: ${bak}`);
|
|
132
|
+
console.log("\n That gives you the green 'done' signal each turn.\n" +
|
|
133
|
+
" For the blue 'working' signal too, source the wrapper:\n" +
|
|
134
|
+
" examples/codex-wrapper.sh (see README → Codex)\n");
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
function install(args) {
|
|
138
|
+
const dryRun = args.includes("--dry-run");
|
|
139
|
+
const target = args.find((a) => !a.startsWith("--"));
|
|
140
|
+
if (target === "claude")
|
|
141
|
+
return installClaude(dryRun);
|
|
142
|
+
if (target === "codex")
|
|
143
|
+
return installCodex(dryRun);
|
|
144
|
+
console.error("usage: andon install <claude|codex> [--dry-run]");
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AndonEvent } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Pure mapping from a Codex notify payload to a board event (no I/O).
|
|
4
|
+
* Returns null for anything other than agent-turn-complete. Exported for testing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mapCodexEvent(payloadArg: string | undefined): AndonEvent | null;
|
|
7
|
+
export declare function notify(payloadArg: string | undefined): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapCodexEvent = mapCodexEvent;
|
|
4
|
+
exports.notify = notify;
|
|
5
|
+
/**
|
|
6
|
+
* `andon notify` — the Codex CLI notifier. Codex passes its event as a single
|
|
7
|
+
* JSON string argument (not stdin). Today Codex only emits
|
|
8
|
+
* `agent-turn-complete`, so this maps that to "done".
|
|
9
|
+
*
|
|
10
|
+
* Field names are read tolerantly (both `last-assistant-message` and
|
|
11
|
+
* `last_assistant_message`, etc.) because Codex's notify schema has used
|
|
12
|
+
* hyphenated keys and may vary by version — the prototype assumed one spelling
|
|
13
|
+
* and would silently miss the message otherwise.
|
|
14
|
+
*/
|
|
15
|
+
const client_1 = require("../client");
|
|
16
|
+
const shared_1 = require("./shared");
|
|
17
|
+
const pick = (o, ...keys) => {
|
|
18
|
+
for (const k of keys)
|
|
19
|
+
if (o[k] != null)
|
|
20
|
+
return o[k];
|
|
21
|
+
return undefined;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Pure mapping from a Codex notify payload to a board event (no I/O).
|
|
25
|
+
* Returns null for anything other than agent-turn-complete. Exported for testing.
|
|
26
|
+
*/
|
|
27
|
+
function mapCodexEvent(payloadArg) {
|
|
28
|
+
if (!payloadArg)
|
|
29
|
+
return null;
|
|
30
|
+
let data;
|
|
31
|
+
try {
|
|
32
|
+
data = JSON.parse(payloadArg);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const type = String(pick(data, "type") ?? "");
|
|
38
|
+
if (type !== "agent-turn-complete")
|
|
39
|
+
return null; // only event Codex sends today
|
|
40
|
+
const cwd = String(pick(data, "cwd") ?? process.cwd());
|
|
41
|
+
const id = process.env.ANDON_SESSION ||
|
|
42
|
+
pick(data, "thread-id", "thread_id", "turn-id") ||
|
|
43
|
+
(0, shared_1.sessionId)("codex", cwd);
|
|
44
|
+
const message = String(pick(data, "last-assistant-message", "last_assistant_message", "message") ?? "")
|
|
45
|
+
.split(/\s+/)
|
|
46
|
+
.join(" ")
|
|
47
|
+
.trim()
|
|
48
|
+
.slice(0, 200);
|
|
49
|
+
return { agent: "codex", id, state: "done", title: (0, shared_1.labelFor)(cwd, "codex"), message };
|
|
50
|
+
}
|
|
51
|
+
async function notify(payloadArg) {
|
|
52
|
+
const ev = mapCodexEvent(payloadArg);
|
|
53
|
+
if (ev)
|
|
54
|
+
await (0, client_1.postEvent)(ev);
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function post(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.post = post;
|
|
4
|
+
/**
|
|
5
|
+
* `andon post <state> <agent> [title] [message]` — manual / wrapper pusher.
|
|
6
|
+
*
|
|
7
|
+
* andon post working codex # codex wrapper: on launch
|
|
8
|
+
* andon post gone codex # codex wrapper: on exit
|
|
9
|
+
* andon post done claude "api" "shipped" # manual board test
|
|
10
|
+
*/
|
|
11
|
+
const client_1 = require("../client");
|
|
12
|
+
const shared_1 = require("./shared");
|
|
13
|
+
const STATES = ["working", "waiting", "done", "error", "idle", "gone"];
|
|
14
|
+
async function post(args) {
|
|
15
|
+
// The codex wrapper calls `andon post` in the foreground on every launch, so
|
|
16
|
+
// pushing status is best-effort and SILENT by default — a missing server must
|
|
17
|
+
// not spam the terminal. Pass --verbose (-v) to see why a push failed.
|
|
18
|
+
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
19
|
+
const [state, agent, title, message] = args.filter((a) => !a.startsWith("-"));
|
|
20
|
+
if (!state || !agent) {
|
|
21
|
+
console.error("usage: andon post <state> <agent> [title] [message]\n" +
|
|
22
|
+
` state: ${STATES.join("|")}`);
|
|
23
|
+
return 2;
|
|
24
|
+
}
|
|
25
|
+
if (!STATES.includes(state)) {
|
|
26
|
+
console.error(`✗ unknown state "${state}" (expected: ${STATES.join("|")})`);
|
|
27
|
+
return 2;
|
|
28
|
+
}
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const r = await (0, client_1.postEvent)({
|
|
31
|
+
agent,
|
|
32
|
+
id: (0, shared_1.sessionId)(agent, cwd),
|
|
33
|
+
state,
|
|
34
|
+
title: title || (0, shared_1.labelFor)(cwd, agent),
|
|
35
|
+
message: message || "",
|
|
36
|
+
});
|
|
37
|
+
if (!r.ok) {
|
|
38
|
+
if (verbose) {
|
|
39
|
+
console.error(`✗ could not reach the andon server (${r.error ?? "unknown"})`);
|
|
40
|
+
}
|
|
41
|
+
return 1; // non-zero for scripts, but quiet
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|