shadok-ai 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.
Files changed (66) hide show
  1. package/README.md +196 -0
  2. package/dist/args.d.ts +8 -0
  3. package/dist/args.js +17 -0
  4. package/dist/args.js.map +1 -0
  5. package/dist/channels.d.ts +61 -0
  6. package/dist/channels.js +169 -0
  7. package/dist/channels.js.map +1 -0
  8. package/dist/cli.d.ts +2 -0
  9. package/dist/cli.js +100 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/config.d.ts +31 -0
  12. package/dist/config.js +59 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/detect.d.ts +17 -0
  15. package/dist/detect.js +41 -0
  16. package/dist/detect.js.map +1 -0
  17. package/dist/extract.d.ts +55 -0
  18. package/dist/extract.js +269 -0
  19. package/dist/extract.js.map +1 -0
  20. package/dist/main.d.ts +2 -0
  21. package/dist/main.js +90 -0
  22. package/dist/main.js.map +1 -0
  23. package/dist/pace.d.ts +33 -0
  24. package/dist/pace.js +53 -0
  25. package/dist/pace.js.map +1 -0
  26. package/dist/retry.d.ts +15 -0
  27. package/dist/retry.js +40 -0
  28. package/dist/retry.js.map +1 -0
  29. package/dist/secrets.d.ts +21 -0
  30. package/dist/secrets.js +71 -0
  31. package/dist/secrets.js.map +1 -0
  32. package/dist/server.d.ts +1 -0
  33. package/dist/server.js +854 -0
  34. package/dist/server.js.map +1 -0
  35. package/dist/session.d.ts +74 -0
  36. package/dist/session.js +244 -0
  37. package/dist/session.js.map +1 -0
  38. package/dist/setup-prompt.d.ts +6 -0
  39. package/dist/setup-prompt.js +25 -0
  40. package/dist/setup-prompt.js.map +1 -0
  41. package/dist/supervisor.d.ts +55 -0
  42. package/dist/supervisor.js +61 -0
  43. package/dist/supervisor.js.map +1 -0
  44. package/dist/tail.d.ts +66 -0
  45. package/dist/tail.js +244 -0
  46. package/dist/tail.js.map +1 -0
  47. package/dist/telegram.d.ts +48 -0
  48. package/dist/telegram.js +565 -0
  49. package/dist/telegram.js.map +1 -0
  50. package/dist/tmux.d.ts +69 -0
  51. package/dist/tmux.js +267 -0
  52. package/dist/tmux.js.map +1 -0
  53. package/dist/update-flag.d.ts +13 -0
  54. package/dist/update-flag.js +36 -0
  55. package/dist/update-flag.js.map +1 -0
  56. package/dist/updater.d.ts +18 -0
  57. package/dist/updater.js +45 -0
  58. package/dist/updater.js.map +1 -0
  59. package/dist/usage.d.ts +23 -0
  60. package/dist/usage.js +125 -0
  61. package/dist/usage.js.map +1 -0
  62. package/dist/worktree.d.ts +63 -0
  63. package/dist/worktree.js +153 -0
  64. package/dist/worktree.js.map +1 -0
  65. package/package.json +44 -0
  66. package/public/index.html +2583 -0
package/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # shadok-ai
2
+
3
+ > **Working on this codebase?** Read [`CLAUDE.md`](CLAUDE.md) (map, build/run,
4
+ > invariants) and [`docs/architecture.md`](docs/architecture.md) first — they
5
+ > are the current source of truth. This README covers the original single-session
6
+ > library/CLI; shadok-ai is now a multi-session web cockpit (see those docs).
7
+
8
+ ## Quick start
9
+
10
+ ```bash
11
+ npx shadok-ai # first run asks once for an optional Telegram bot token
12
+ ```
13
+
14
+ Open **http://localhost:3789**. To drive agents from Telegram, send the bot to a
15
+ group with Topics enabled and type `/setup` there; then `/spawn <name>` per agent.
16
+ Keep it current with `/update` (fetches `@latest` and respawns itself). Flags:
17
+ `--port <n>`, `--no-telegram`, `--version`.
18
+
19
+ ---
20
+
21
+ Drives a **Claude Code TUI** session (the interactive `claude` CLI) from Node.js, through a pseudo-terminal.
22
+
23
+ > ⚠️ The Claude Code TUI is not a stable API: a CLI update can break the
24
+ > detection heuristics (`❯`, `⏺`, `esc to interrupt` markers).
25
+ > For production use, prefer the
26
+ > [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk)
27
+ > or `claude -p --output-format stream-json`.
28
+
29
+ ## How it works
30
+
31
+ - **node-pty** spawns `claude` inside a real pseudo-terminal (the TUI believes it talks to a human);
32
+ - **@xterm/headless** replays the ANSI stream into a virtual screen: we read
33
+ the screen as it would be displayed instead of parsing escape sequences;
34
+ - the **responses to terminal queries** (cursor position `\x1b[?6n`,
35
+ identification `\x1b[c`…) generated by xterm are forwarded back to the
36
+ PTY — without this, the TUI ignores every keystroke;
37
+ - prompts are sent via **bracketed paste** (`\x1b[200~…\x1b[201~`) with
38
+ retries, because the TUI flushes stdin received during its initialization;
39
+ - the "Claude is done" state is detected heuristically: no
40
+ `esc to interrupt` marker + screen stable for N ms.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ npm install
46
+ npm run build
47
+ ```
48
+
49
+ The `postinstall` script fixes the executable bit of node-pty's
50
+ `spawn-helper` (a known npm prebuilds bug on macOS).
51
+
52
+ ## CLI
53
+
54
+ ```bash
55
+ node dist/cli.js [options] "<prompt>"
56
+
57
+ --cwd <dir> working directory of the claude session
58
+ --continue, -c resume the latest session of this directory
59
+ --resume <id>, -r resume a specific session (id printed at end of run)
60
+ --watch mirror the TUI live on stdout
61
+ --keep keep the session open at the end
62
+ --timeout <sec> max wait for the response (default 600)
63
+ ```
64
+
65
+ Without `--continue`/`--resume`, every run starts a **new** session.
66
+ The session id is printed at the end of the run (found in
67
+ `~/.claude/projects/<encoded cwd>/`, like any Claude Code session):
68
+
69
+ ```bash
70
+ node dist/cli.js --cwd ~/my-project "Explain this project's structure"
71
+ # ▶ session: 5fe046dd-…
72
+
73
+ node dist/cli.js --cwd ~/my-project --resume 5fe046dd-… "Now refactor it"
74
+ node dist/cli.js --cwd ~/my-project -c "Continue on the latest session"
75
+ ```
76
+
77
+ ## Web interface
78
+
79
+ ```bash
80
+ npm run web # http://localhost:3789 (PORT=… to change)
81
+ ```
82
+
83
+ A chat in the browser driving persistent TUI sessions, in parallel through
84
+ channels:
85
+
86
+ - **channels** (left column): "+ new channel" opens a new independent
87
+ claude session (one WebSocket connection each). Each channel's LED shows
88
+ its state (green: ready, amber: responding, red: ended); the × closes the
89
+ channel and detaches from its session; double-click a name to rename it
90
+ (the name is remembered by session id and reapplied on resume). The
91
+ header, composer and Terminal view follow the active channel. Channels
92
+ are remembered (localStorage): on the next page load each one is restored
93
+ automatically — session resumed via `--resume` and history replayed.
94
+ Closing a channel (×) removes it from the remembered list;
95
+ - **interactive dialogs**: when the TUI shows a choice (multiple-choice
96
+ question, permission prompt…), the options appear as clickable buttons in
97
+ the chat. Single select: one click validates. Multi-select (`[ ]`
98
+ checkboxes in the TUI): clicks toggle (the displayed state is re-read
99
+ from the TUI screen), then "Submit selection" confirms (Tab → Submit page
100
+ → Enter). The composer is locked while a choice is pending. The
101
+ "Type something" option becomes a free-form text input inside the dialog
102
+ bubble;
103
+ - **history on resume**: when resuming a session (`--continue` or
104
+ `--resume`), the transcript is re-read from the session's `.jsonl` and
105
+ replayed in the chat (last 100 turns, dimmed). The markdown of the
106
+ responses is rendered as HTML (tables, code, lists) via `marked`, served
107
+ locally;
108
+ - **"engine room"** (Terminal button): the raw TUI screen live, with
109
+ clickable keys (Esc, Enter, arrows, 1/2/3, y/n…) to answer permission
110
+ dialogs, and "Resync chat" to resynchronize the chat after a manual
111
+ intervention.
112
+
113
+ ### WebSocket protocol (to replace the interface)
114
+
115
+ The interface is a plain static page: all the intelligence lives server-side.
116
+ Any client (another front-end, a Slack bot, a script…) can replace it by
117
+ speaking this JSON protocol on `ws://…/ws`.
118
+
119
+ **Shared sessions**: the server keeps a single claude process per session
120
+ id. If several clients (tabs, browsers, interfaces) "start" the same id,
121
+ they attach to the same process and all receive the same events — other
122
+ clients' prompts (`prompt-echo`), answers, dialogs, screen. Closing a
123
+ connection detaches the client; the process is only stopped when the last
124
+ client detaches (session stays resumable) or on an explicit `stop` (ends it
125
+ for everyone).
126
+
127
+ **client → server**
128
+
129
+ | Message | Purpose |
130
+ |---|---|
131
+ | `{type:"start", cwd?, resume?, continue?}` | starts or attaches to the session (once per connection) |
132
+ | `{type:"prompt", text}` | sends a prompt |
133
+ | `{type:"choose", n}` | single-select dialog: picks and validates option n |
134
+ | `{type:"toggle", n}` | multi-select dialog: toggles option n |
135
+ | `{type:"confirm"}` | multi-select dialog: submits the selection |
136
+ | `{type:"freetext", n, text}` | "Type something" option: sends a free-form answer |
137
+ | `{type:"key", key}` | raw keystroke (`enter`, `escape`, `up`, `down`, `tab`, `ctrl-c`, or a single character) |
138
+ | `{type:"settle"}` | after a manual intervention: waits for the turn to finish |
139
+ | `{type:"stop"}` | closes the session cleanly (/exit) for all clients |
140
+
141
+ **server → client**
142
+
143
+ | Message | Purpose |
144
+ |---|---|
145
+ | `{type:"ready", sessionId, cwd}` | session started (or attached) |
146
+ | `{type:"working"}` | turn in progress |
147
+ | `{type:"prompt-echo", text}` | prompt sent by another client of the session |
148
+ | `{type:"answer", text, sessionId}` | response extracted from the transcript |
149
+ | `{type:"dialog", question, options:[{n,label,hint,checked?}], multi}` | choice pending |
150
+ | `{type:"history", turns:[{role,text}]}` | transcript replayed when resuming/attaching |
151
+ | `{type:"screen", text, working}` | rendered TUI screen (whenever it changes) |
152
+ | `{type:"error", message}` / `{type:"exited", code}` / `{type:"stopped"}` | errors and termination |
153
+
154
+ ## Library
155
+
156
+ ```js
157
+ import { PtyPilot } from "shadok-ai";
158
+
159
+ const pilot = new PtyPilot({ cwd: "/my/project" });
160
+ pilot.start();
161
+ await pilot.waitForIdle(); // TUI ready
162
+
163
+ await pilot.submit("Fix the bug in auth.ts");
164
+ await pilot.waitForIdle({ timeoutMs: 600_000 });
165
+ console.log(pilot.screen()); // rendered screen (visible transcript)
166
+
167
+ await pilot.submit("Now add a test"); // same session, second turn
168
+ await pilot.waitForIdle();
169
+
170
+ await pilot.stop(); // clean /exit, kill as fallback
171
+ ```
172
+
173
+ Main API:
174
+
175
+ | Method | Purpose |
176
+ |---|---|
177
+ | `start()` / `stop()` / `kill()` | process lifecycle |
178
+ | `submit(text)` | types a prompt + Enter, with verification and retries |
179
+ | `write(text)` / `press(key)` | low-level keystrokes (`enter`, `escape`, `up`, `tab`, `ctrl-c`…) |
180
+ | `screen()` / `fullBuffer()` | rendered screen / full buffer |
181
+ | `waitForIdle({stableMs, timeoutMs})` | waits for the end of a turn |
182
+ | `waitFor(predicate)` | waits for an arbitrary screen condition (dialogs, permissions…) |
183
+ | `isWorking()` | true while Claude is working |
184
+ | `onData(cb)` / `onExit(cb)` | raw ANSI stream (mirror mode) / process exit |
185
+
186
+ `examples/two-turns.mjs` shows a two-turn conversation;
187
+ `debug/probe.mjs` logs the terminal sequences exchanged (useful when a CLI
188
+ update breaks the detection).
189
+
190
+ ## Known limits
191
+
192
+ - Every session launched consumes your Claude quota like a normal session.
193
+ - Dialogs the chat cannot handle in one click are managed through the
194
+ engine room (`waitFor()` + `press()` in library mode).
195
+ - The TUI runs in the alternate screen: `fullBuffer()` ≈ visible screen;
196
+ very long responses scroll out of view (increase `rows` if needed).
package/dist/args.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export interface Args {
2
+ port?: number;
3
+ noTelegram: boolean;
4
+ help: boolean;
5
+ version: boolean;
6
+ }
7
+ /** Parse the CLI flags. Unknown flags are ignored (forward-compatible). */
8
+ export declare function parseArgs(argv: string[]): Args;
package/dist/args.js ADDED
@@ -0,0 +1,17 @@
1
+ /** Parse the CLI flags. Unknown flags are ignored (forward-compatible). */
2
+ export function parseArgs(argv) {
3
+ const a = { noTelegram: false, help: false, version: false };
4
+ for (let i = 0; i < argv.length; i++) {
5
+ const arg = argv[i];
6
+ if (arg === "--port" || arg === "-p")
7
+ a.port = Number(argv[++i]);
8
+ else if (arg === "--no-telegram")
9
+ a.noTelegram = true;
10
+ else if (arg === "--help" || arg === "-h")
11
+ a.help = true;
12
+ else if (arg === "--version" || arg === "-v")
13
+ a.version = true;
14
+ }
15
+ return a;
16
+ }
17
+ //# sourceMappingURL=args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../src/args.ts"],"names":[],"mappings":"AAOA,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,MAAM,CAAC,GAAS,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;YAAE,CAAC,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;aAC5D,IAAI,GAAG,KAAK,eAAe;YAAE,CAAC,CAAC,UAAU,GAAG,IAAI,CAAC;aACjD,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;YAAE,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC;aACpD,IAAI,GAAG,KAAK,WAAW,IAAI,GAAG,KAAK,IAAI;YAAE,CAAC,CAAC,OAAO,GAAG,IAAI,CAAC;IACjE,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,61 @@
1
+ /** A Telegram chat/topic a channel is bound to (folded into the channel). */
2
+ export interface TgBinding {
3
+ chatId: number;
4
+ threadId?: number;
5
+ }
6
+ /** A remembered session — the ONE registry, whatever created it (web or
7
+ * Telegram). Enough to restore, resume, and route it. */
8
+ export interface Channel {
9
+ sessionId: string;
10
+ cwd: string;
11
+ name?: string;
12
+ branch?: string | null;
13
+ /** Repo the worktree belongs to, to recreate a reclaimed checkout. */
14
+ repo?: string;
15
+ /** Tab group id (client-owned metadata). */
16
+ group?: number | null;
17
+ /** Present iff the session is bound to a Telegram chat/topic. */
18
+ telegram?: TgBinding | null;
19
+ }
20
+ export declare function loadChannels(): Channel[];
21
+ export declare function saveChannels(list: Channel[]): void;
22
+ /**
23
+ * Insert or update a channel by sessionId, shallow-merging the given fields
24
+ * (an `undefined` field never clobbers an existing value). The server calls
25
+ * this whenever a session reaches `ready`, so every session — web or Telegram —
26
+ * lands in the one list.
27
+ */
28
+ /** Pure core of upsertChannel — returns a new list; exported for testing. */
29
+ export declare function upsertInto(list: Channel[], patch: Partial<Channel> & {
30
+ sessionId: string;
31
+ }): Channel[];
32
+ export declare function upsertChannel(patch: Partial<Channel> & {
33
+ sessionId: string;
34
+ }): void;
35
+ export declare function removeChannel(sessionId: string): void;
36
+ /** Pure lookup of the channel bound to a Telegram chat/topic. */
37
+ export declare function findTelegramChannel(list: Channel[], chatId: number, threadId?: number): Channel | undefined;
38
+ /** The channel bound to a given Telegram chat/topic, or undefined. */
39
+ export declare function channelForTelegram(chatId: number, threadId?: number): Channel | undefined;
40
+ /**
41
+ * Pure core of the PUT merge (exported for testing). The client drives order
42
+ * and its own metadata (name, group); server-owned fields are preserved per
43
+ * sessionId, and any stored channel the client omitted is kept when its session
44
+ * is live or it has a Telegram binding — so persistence never drops a
45
+ * live/Telegram session (invariant #6).
46
+ */
47
+ export declare function mergeChannels(stored: Channel[], clientList: Channel[], liveIds: Set<string>): Channel[];
48
+ export declare function mergeClientChannels(clientList: Channel[], liveIds: Set<string>): Channel[];
49
+ /** Tab groups (id, name, collapsed, order), persisted the same way as channels. */
50
+ export declare function loadGroups(): any[];
51
+ export declare function saveGroups(list: any[]): void;
52
+ /** The single group this instance is bound to (one board per instance), or null.
53
+ * Instance-level, not a session — kept separate from the channel registry. */
54
+ export declare function loadTgGroup(): number | null;
55
+ export declare function saveTgGroup(groupId: number | null): void;
56
+ /**
57
+ * One-time migration of the old separate `…-telegram.json` bindings into the
58
+ * channel registry. Idempotent: renames the file once folded so it's never
59
+ * re-applied. cwd/name are filled in later when the session next reaches ready.
60
+ */
61
+ export declare function migrateTgBindings(): void;
@@ -0,0 +1,169 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ /** Fields the server owns; a browser PUT must never overwrite or drop them. */
5
+ const SERVER_OWNED = ["cwd", "branch", "repo", "telegram"];
6
+ /**
7
+ * The registry is stored server-side, keyed by the directory the server was
8
+ * launched from — so the set of sessions survives a wiped browser, another
9
+ * device, a restart, or a reboot. One file per launch dir:
10
+ * ~/.shadok-ai/channels/<encoded cwd>.json.
11
+ */
12
+ function storeFile(kind) {
13
+ const enc = process.cwd().replace(/[^a-zA-Z0-9]/g, "-");
14
+ const suffix = kind === "channels" ? "" : "-" + kind;
15
+ return path.join(os.homedir(), ".shadok-ai", "channels", enc + suffix + ".json");
16
+ }
17
+ function readJson(kind) {
18
+ try {
19
+ const v = JSON.parse(fs.readFileSync(storeFile(kind), "utf8"));
20
+ return Array.isArray(v) ? v : [];
21
+ }
22
+ catch {
23
+ return [];
24
+ }
25
+ }
26
+ function writeJson(kind, value) {
27
+ const f = storeFile(kind);
28
+ try {
29
+ fs.mkdirSync(path.dirname(f), { recursive: true });
30
+ fs.writeFileSync(f, JSON.stringify(value, null, 2));
31
+ }
32
+ catch {
33
+ // best effort: losing the persisted list is non-fatal
34
+ }
35
+ }
36
+ export function loadChannels() {
37
+ return readJson("channels").filter((c) => c && typeof c.sessionId === "string");
38
+ }
39
+ export function saveChannels(list) {
40
+ writeJson("channels", list);
41
+ }
42
+ /**
43
+ * Insert or update a channel by sessionId, shallow-merging the given fields
44
+ * (an `undefined` field never clobbers an existing value). The server calls
45
+ * this whenever a session reaches `ready`, so every session — web or Telegram —
46
+ * lands in the one list.
47
+ */
48
+ /** Pure core of upsertChannel — returns a new list; exported for testing. */
49
+ export function upsertInto(list, patch) {
50
+ const out = list.map((c) => ({ ...c }));
51
+ const cur = out.find((c) => c.sessionId === patch.sessionId);
52
+ if (!cur) {
53
+ out.push({ cwd: "", ...patch });
54
+ }
55
+ else {
56
+ for (const [k, v] of Object.entries(patch))
57
+ if (v !== undefined)
58
+ cur[k] = v;
59
+ }
60
+ return out;
61
+ }
62
+ export function upsertChannel(patch) {
63
+ saveChannels(upsertInto(loadChannels(), patch));
64
+ }
65
+ export function removeChannel(sessionId) {
66
+ saveChannels(loadChannels().filter((c) => c.sessionId !== sessionId));
67
+ }
68
+ /** Pure lookup of the channel bound to a Telegram chat/topic. */
69
+ export function findTelegramChannel(list, chatId, threadId) {
70
+ return list.find((c) => c.telegram && c.telegram.chatId === chatId && (c.telegram.threadId ?? undefined) === threadId);
71
+ }
72
+ /** The channel bound to a given Telegram chat/topic, or undefined. */
73
+ export function channelForTelegram(chatId, threadId) {
74
+ return findTelegramChannel(loadChannels(), chatId, threadId);
75
+ }
76
+ /**
77
+ * Pure core of the PUT merge (exported for testing). The client drives order
78
+ * and its own metadata (name, group); server-owned fields are preserved per
79
+ * sessionId, and any stored channel the client omitted is kept when its session
80
+ * is live or it has a Telegram binding — so persistence never drops a
81
+ * live/Telegram session (invariant #6).
82
+ */
83
+ export function mergeChannels(stored, clientList, liveIds) {
84
+ const byId = new Map(stored.map((c) => [c.sessionId, c]));
85
+ const result = [];
86
+ const seen = new Set();
87
+ for (const c of Array.isArray(clientList) ? clientList : []) {
88
+ if (!c || typeof c.sessionId !== "string")
89
+ continue;
90
+ seen.add(c.sessionId);
91
+ const prev = byId.get(c.sessionId);
92
+ if (!prev) {
93
+ result.push(c); // a new channel the client just created
94
+ continue;
95
+ }
96
+ const merged = { ...c };
97
+ for (const k of SERVER_OWNED)
98
+ if (prev[k] !== undefined)
99
+ merged[k] = prev[k];
100
+ result.push(merged);
101
+ }
102
+ for (const c of stored) {
103
+ if (seen.has(c.sessionId))
104
+ continue;
105
+ if (liveIds.has(c.sessionId) || c.telegram)
106
+ result.push(c); // never erase live/Telegram
107
+ }
108
+ return result;
109
+ }
110
+ export function mergeClientChannels(clientList, liveIds) {
111
+ const result = mergeChannels(loadChannels(), clientList, liveIds);
112
+ saveChannels(result);
113
+ return result;
114
+ }
115
+ /** Tab groups (id, name, collapsed, order), persisted the same way as channels. */
116
+ export function loadGroups() {
117
+ return readJson("groups");
118
+ }
119
+ export function saveGroups(list) {
120
+ writeJson("groups", list);
121
+ }
122
+ /** The single group this instance is bound to (one board per instance), or null.
123
+ * Instance-level, not a session — kept separate from the channel registry. */
124
+ export function loadTgGroup() {
125
+ const v = readJson("telegram-group")[0];
126
+ return typeof v === "number" ? v : null;
127
+ }
128
+ export function saveTgGroup(groupId) {
129
+ writeJson("telegram-group", groupId == null ? [] : [groupId]);
130
+ }
131
+ /**
132
+ * One-time migration of the old separate `…-telegram.json` bindings into the
133
+ * channel registry. Idempotent: renames the file once folded so it's never
134
+ * re-applied. cwd/name are filled in later when the session next reaches ready.
135
+ */
136
+ export function migrateTgBindings() {
137
+ const f = storeFile("telegram");
138
+ let raw;
139
+ try {
140
+ raw = fs.readFileSync(f, "utf8");
141
+ }
142
+ catch {
143
+ return; // already migrated or never existed
144
+ }
145
+ let bindings;
146
+ try {
147
+ bindings = JSON.parse(raw);
148
+ }
149
+ catch {
150
+ return;
151
+ }
152
+ if (Array.isArray(bindings)) {
153
+ for (const b of bindings) {
154
+ if (b && typeof b.sessionId === "string" && typeof b.chatId === "number") {
155
+ upsertChannel({
156
+ sessionId: b.sessionId,
157
+ telegram: { chatId: b.chatId, ...(typeof b.threadId === "number" ? { threadId: b.threadId } : {}) },
158
+ });
159
+ }
160
+ }
161
+ }
162
+ try {
163
+ fs.renameSync(f, f + ".migrated");
164
+ }
165
+ catch {
166
+ /* best effort */
167
+ }
168
+ }
169
+ //# sourceMappingURL=channels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channels.js","sourceRoot":"","sources":["../src/channels.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { extractResponse, findSessionId } from "./extract.js";
3
+ import { PtyPilot } from "./session.js";
4
+ function usage() {
5
+ console.error(`Usage: shadok-ai [options] "<prompt>"
6
+
7
+ Sends a prompt to a PTY-driven Claude Code TUI session and prints the
8
+ response once the session becomes idle again.
9
+
10
+ Options:
11
+ --cwd <dir> Working directory of the session (default: current cwd)
12
+ --continue, -c Resume the latest session of this directory
13
+ --resume <id>, -r Resume a specific session (id printed at end of run)
14
+ --watch Mirror the TUI live on stdout
15
+ --keep Keep the session open at the end (Ctrl-C to quit)
16
+ --timeout <sec> Max wait for the response (default: 600)
17
+ `);
18
+ process.exit(1);
19
+ }
20
+ const argv = process.argv.slice(2);
21
+ let cwd = process.cwd();
22
+ let watch = false;
23
+ let keep = false;
24
+ let timeoutSec = 600;
25
+ let continueSession = false;
26
+ let resumeId = null;
27
+ const rest = [];
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const a = argv[i];
30
+ if (a === "--cwd")
31
+ cwd = argv[++i] ?? usage();
32
+ else if (a === "--continue" || a === "-c")
33
+ continueSession = true;
34
+ else if (a === "--resume" || a === "-r")
35
+ resumeId = argv[++i] ?? usage();
36
+ else if (a === "--watch")
37
+ watch = true;
38
+ else if (a === "--keep")
39
+ keep = true;
40
+ else if (a === "--timeout")
41
+ timeoutSec = Number(argv[++i] ?? usage());
42
+ else if (a === "--help" || a === "-h")
43
+ usage();
44
+ else
45
+ rest.push(a);
46
+ }
47
+ const prompt = rest.join(" ").trim();
48
+ if (!prompt)
49
+ usage();
50
+ if (continueSession && resumeId) {
51
+ console.error("--continue and --resume are mutually exclusive.");
52
+ usage();
53
+ }
54
+ const claudeArgs = [];
55
+ if (continueSession)
56
+ claudeArgs.push("--continue");
57
+ if (resumeId)
58
+ claudeArgs.push("--resume", resumeId);
59
+ const pilot = new PtyPilot({ cwd, args: claudeArgs });
60
+ async function main() {
61
+ console.error("▶ starting claude…");
62
+ pilot.start();
63
+ if (watch)
64
+ pilot.onData((chunk) => process.stdout.write(chunk));
65
+ // Wait for the TUI to be ready (stable screen, no spinner).
66
+ let screen = await pilot.waitForIdle({ stableMs: 1200, timeoutMs: 60_000 });
67
+ // First launch in a directory: trust dialog.
68
+ if (/do you trust the files/i.test(screen)) {
69
+ console.error("▶ trust dialog detected, accepting…");
70
+ pilot.press("enter");
71
+ screen = await pilot.waitForIdle({ stableMs: 1200, timeoutMs: 30_000 });
72
+ }
73
+ console.error(`▶ sending prompt: ${prompt}`);
74
+ await pilot.submit(prompt);
75
+ await pilot.waitForIdle({ stableMs: 2000, timeoutMs: timeoutSec * 1000 });
76
+ console.error("▶ response received:\n");
77
+ process.stdout.write(extractResponse(pilot.fullBuffer(), prompt) + "\n");
78
+ const sessionId = findSessionId(cwd);
79
+ if (sessionId) {
80
+ console.error(`\n▶ session: ${sessionId}`);
81
+ console.error(` resume with: shadok-ai --resume ${sessionId} "<prompt>"`);
82
+ }
83
+ if (keep) {
84
+ console.error("\n▶ session kept open (--keep). Ctrl-C to quit.");
85
+ process.on("SIGINT", async () => {
86
+ await pilot.stop();
87
+ process.exit(0);
88
+ });
89
+ }
90
+ else {
91
+ await pilot.stop();
92
+ process.exit(0);
93
+ }
94
+ }
95
+ main().catch((err) => {
96
+ console.error(err instanceof Error ? err.message : err);
97
+ pilot.kill();
98
+ process.exit(1);
99
+ });
100
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Global CLI config, stored at ~/.shadok-ai/config.json (mode 600). Holds the
3
+ * Telegram bot token and the port. Replaces the old telegram.env shell hack:
4
+ * the token now lives here and is passed to the server child via env.
5
+ *
6
+ * `telegramToken` semantics:
7
+ * undefined → never asked yet (first run should prompt)
8
+ * null → asked and deliberately skipped (never prompt again)
9
+ * string → the token
10
+ */
11
+ export interface Config {
12
+ telegramToken?: string | null;
13
+ port?: number;
14
+ }
15
+ export declare const SHADOK_DIR: string;
16
+ export declare function loadConfig(): Config;
17
+ export declare function saveConfig(cfg: Config): void;
18
+ /**
19
+ * One-time migration of the legacy ~/.shadok-ai/telegram.env
20
+ * (`TELEGRAM_BOT_TOKEN=...`) into config.json. Returns the migrated token, or
21
+ * null if there was nothing to migrate. The env file is left on disk but no
22
+ * longer read by anything.
23
+ */
24
+ export declare function migrateLegacyEnv(cfg: Config): string | null;
25
+ /** Pull TELEGRAM_BOT_TOKEN out of a shell-style env file body. */
26
+ export declare function parseLegacyToken(raw: string): string | null;
27
+ /**
28
+ * The effective token to hand the server: an explicit env var always wins,
29
+ * otherwise the configured value (a null/undefined config means "no token").
30
+ */
31
+ export declare function effectiveToken(cfg: Config, env?: NodeJS.ProcessEnv): string | null;