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/dist/config.js ADDED
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export const SHADOK_DIR = path.join(os.homedir(), ".shadok-ai");
5
+ const CONFIG_FILE = path.join(SHADOK_DIR, "config.json");
6
+ const LEGACY_ENV = path.join(SHADOK_DIR, "telegram.env");
7
+ export function loadConfig() {
8
+ try {
9
+ const v = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
10
+ return v && typeof v === "object" ? v : {};
11
+ }
12
+ catch {
13
+ return {};
14
+ }
15
+ }
16
+ export function saveConfig(cfg) {
17
+ fs.mkdirSync(SHADOK_DIR, { recursive: true });
18
+ // 600: the token is a secret. Write then chmod (umask-independent).
19
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 });
20
+ fs.chmodSync(CONFIG_FILE, 0o600);
21
+ }
22
+ /**
23
+ * One-time migration of the legacy ~/.shadok-ai/telegram.env
24
+ * (`TELEGRAM_BOT_TOKEN=...`) into config.json. Returns the migrated token, or
25
+ * null if there was nothing to migrate. The env file is left on disk but no
26
+ * longer read by anything.
27
+ */
28
+ export function migrateLegacyEnv(cfg) {
29
+ if (cfg.telegramToken !== undefined)
30
+ return null; // already decided
31
+ let raw;
32
+ try {
33
+ raw = fs.readFileSync(LEGACY_ENV, "utf8");
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ const token = parseLegacyToken(raw);
39
+ if (!token)
40
+ return null;
41
+ cfg.telegramToken = token;
42
+ saveConfig(cfg);
43
+ return token;
44
+ }
45
+ /** Pull TELEGRAM_BOT_TOKEN out of a shell-style env file body. */
46
+ export function parseLegacyToken(raw) {
47
+ const m = raw.match(/TELEGRAM_BOT_TOKEN\s*=\s*["']?([^"'\s]+)/);
48
+ return m ? m[1] : null;
49
+ }
50
+ /**
51
+ * The effective token to hand the server: an explicit env var always wins,
52
+ * otherwise the configured value (a null/undefined config means "no token").
53
+ */
54
+ export function effectiveToken(cfg, env = process.env) {
55
+ if (env.TELEGRAM_BOT_TOKEN)
56
+ return env.TELEGRAM_BOT_TOKEN;
57
+ return cfg.telegramToken ?? null;
58
+ }
59
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAiB7B,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AAChE,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;AAEzD,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;QAC3D,OAAO,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC9C,oEAAoE;IACpE,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC7E,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,GAAG,CAAC,aAAa,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,CAAC,kBAAkB;IACpE,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,GAAG,CAAC,aAAa,GAAG,KAAK,CAAC;IAC1B,UAAU,CAAC,GAAG,CAAC,CAAC;IAChB,OAAO,KAAK,CAAC;AACf,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAChE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW,EAAE,GAAG,GAAsB,OAAO,CAAC,GAAG;IAC9E,IAAI,GAAG,CAAC,kBAAkB;QAAE,OAAO,GAAG,CAAC,kBAAkB,CAAC;IAC1D,OAAO,GAAG,CAAC,aAAa,IAAI,IAAI,CAAC;AACnC,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Detection of the "Claude is working" state from the rendered TUI screen.
3
+ *
4
+ * Kept free of any dependency so it can be unit-tested directly
5
+ * (debug/detect.test.mjs) against captured screen fixtures.
6
+ */
7
+ /** True when the screen indicates that Claude is currently working. */
8
+ export declare function screenShowsWork(screen: string): boolean;
9
+ /**
10
+ * True while the text we just typed is still sitting in the input box (the
11
+ * last "❯ …" line). Used to confirm a submit: before Enter the probe is in the
12
+ * box; once Enter is accepted the box clears, so `!inputHasProbe` means "sent".
13
+ * This is robust to a fast turn scrolling the echo out of the transcript —
14
+ * unlike looking for the probe anywhere on screen, which false-negatived and
15
+ * dumped the whole screen as an error.
16
+ */
17
+ export declare function inputHasProbe(screen: string, probe: string): boolean;
package/dist/detect.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Detection of the "Claude is working" state from the rendered TUI screen.
3
+ *
4
+ * Kept free of any dependency so it can be unit-tested directly
5
+ * (debug/detect.test.mjs) against captured screen fixtures.
6
+ */
7
+ /**
8
+ * Marker displayed by Claude Code TUIs while working. The genuine one always
9
+ * sits in the status group, right after a "·" separator or an opening "(",
10
+ * e.g. "(… · esc to interrupt)". Requiring that prefix means prose that merely
11
+ * *mentions* the phrase — Claude explaining shadok-ai's own turn detection,
12
+ * quoted or not — never reads as "working".
13
+ */
14
+ const ESC_TO_INTERRUPT = /[·(]\s*esc to interrupt/i;
15
+ /**
16
+ * Newer TUIs drop the "esc to interrupt" hint; the live spinner line is then
17
+ * the only working signal: a glyph at column 0 followed by a status and an
18
+ * "(<elapsed> · …)" group with the elapsed FIRST, e.g.
19
+ * ✽ Jitterbugging… (4m 26s · ↓ 7.1k tokens · …)
20
+ * A finished turn renders past tense without parens ("✻ Baked for 8m 20s"),
21
+ * and completion lines put the elapsed LAST ("(… · 5m 40s)"): no match.
22
+ */
23
+ const SPINNER_STATUS = /^[^\s\w].{0,80}?\(\s*(?:\d+h\s*)?(?:\d+m\s*)?\d+s\s*·/m;
24
+ /** True when the screen indicates that Claude is currently working. */
25
+ export function screenShowsWork(screen) {
26
+ return ESC_TO_INTERRUPT.test(screen) || SPINNER_STATUS.test(screen);
27
+ }
28
+ /**
29
+ * True while the text we just typed is still sitting in the input box (the
30
+ * last "❯ …" line). Used to confirm a submit: before Enter the probe is in the
31
+ * box; once Enter is accepted the box clears, so `!inputHasProbe` means "sent".
32
+ * This is robust to a fast turn scrolling the echo out of the transcript —
33
+ * unlike looking for the probe anywhere on screen, which false-negatived and
34
+ * dumped the whole screen as an error.
35
+ */
36
+ export function inputHasProbe(screen, probe) {
37
+ const promptLines = screen.split("\n").filter((l) => l.trimStart().startsWith("❯"));
38
+ const inputLine = promptLines[promptLines.length - 1] ?? "";
39
+ return inputLine.includes(probe);
40
+ }
41
+ //# sourceMappingURL=detect.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.js","sourceRoot":"","sources":["../src/detect.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,0BAA0B,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,cAAc,GAClB,wDAAwD,CAAC;AAE3D,uEAAuE;AACvE,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACtE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAa;IACzD,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACpF,MAAM,SAAS,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5D,OAAO,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Extracts the response: everything after the "❯ <prompt>" echo in the
3
+ * transcript, minus the final input box and status lines.
4
+ */
5
+ export declare function extractResponse(buffer: string, prompt: string): string;
6
+ export interface TuiDialogOption {
7
+ n: number;
8
+ label: string;
9
+ hint: string;
10
+ /** Multi-select only: state of the [ ] / [✔] checkbox. */
11
+ checked?: boolean;
12
+ }
13
+ export interface TuiDialog {
14
+ question: string;
15
+ options: TuiDialogOption[];
16
+ /**
17
+ * True for multi-select questions: digits toggle checkboxes, submission
18
+ * goes through Tab (Submit page) then Enter. In single-select mode,
19
+ * pressing the digit selects and validates directly.
20
+ */
21
+ multi: boolean;
22
+ }
23
+ /**
24
+ * Detects an interactive TUI dialog (multiple-choice question, permission
25
+ * prompt…): numbered options, one of which carries the "❯" selector.
26
+ */
27
+ export declare function detectDialog(screen: string): TuiDialog | null;
28
+ export interface HistoryTurn {
29
+ role: "user" | "assistant";
30
+ text: string;
31
+ }
32
+ /**
33
+ * Reads a session transcript back from its .jsonl file
34
+ * (~/.claude/projects/<encoded cwd>/<session-id>.jsonl) so the history can
35
+ * be replayed when resuming the session.
36
+ */
37
+ export declare function loadHistory(cwd: string, sessionId: string): HistoryTurn[];
38
+ export interface SessionInfo {
39
+ id: string;
40
+ /** Last activity, in ms since epoch (file mtime). */
41
+ mtime: number;
42
+ /** First real user prompt of the session, truncated. */
43
+ preview: string;
44
+ }
45
+ /**
46
+ * Lists the resumable sessions of a directory (newest first), with the
47
+ * first user prompt as a preview so a session can be recognized by more
48
+ * than its id.
49
+ */
50
+ export declare function listSessions(cwd: string): SessionInfo[];
51
+ /**
52
+ * Finds the id of the most recent session of a directory: Claude Code
53
+ * writes each session to ~/.claude/projects/<encoded cwd>/<session-id>.jsonl.
54
+ */
55
+ export declare function findSessionId(cwd: string): string | null;
@@ -0,0 +1,269 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { newestTranscriptById } from "./tail.js";
5
+ /**
6
+ * Extracts the response: everything after the "❯ <prompt>" echo in the
7
+ * transcript, minus the final input box and status lines.
8
+ */
9
+ export function extractResponse(buffer, prompt) {
10
+ const lines = buffer.split("\n");
11
+ const probe = prompt.slice(0, 15);
12
+ let start = -1;
13
+ // The prompt echo in the transcript starts with "❯ "; so does the (empty)
14
+ // input box at the bottom — take the last occurrence containing the
15
+ // beginning of the prompt.
16
+ for (let i = lines.length - 1; i >= 0; i--) {
17
+ if (/^\s*[❯>]/.test(lines[i]) && lines[i].includes(probe)) {
18
+ start = i + 1;
19
+ break;
20
+ }
21
+ }
22
+ if (start === -1)
23
+ return buffer.trim();
24
+ // A long prompt echo spans several lines: the response starts at the
25
+ // first "⏺" marker that follows, when there is one.
26
+ for (let i = start; i < lines.length; i++) {
27
+ if (/^\s*⏺/.test(lines[i])) {
28
+ start = i;
29
+ break;
30
+ }
31
+ }
32
+ const noise = (l) => /^\s*✻/.test(l) || /·\s*\/effort\s*$/.test(l) || /esc to interrupt/i.test(l);
33
+ // Cut the input area (─── separator followed by ❯) when present.
34
+ let end = lines.length;
35
+ for (let i = start; i < lines.length; i++) {
36
+ if (/^─{10,}/.test(lines[i].trim())) {
37
+ end = i;
38
+ break;
39
+ }
40
+ }
41
+ return lines
42
+ .slice(start, end)
43
+ .filter((l) => !noise(l))
44
+ .join("\n")
45
+ .trim();
46
+ }
47
+ /**
48
+ * Detects an interactive TUI dialog (multiple-choice question, permission
49
+ * prompt…): numbered options, one of which carries the "❯" selector.
50
+ */
51
+ export function detectDialog(screen) {
52
+ // AskUserQuestion can render a preview (chart, code…) in a right-hand column
53
+ // on the SAME lines as the options. Strip that column — a run of ≥2 spaces
54
+ // followed by a box-drawing char — so option labels don't absorb it.
55
+ const lines = screen
56
+ .split("\n")
57
+ .map((l) => l.replace(/\s{2,}[│┌└├┐┘┤┬┴┼─╭╮╰╯].*$/u, "").replace(/\s+$/, ""));
58
+ const optionRe = /^\s*(❯\s*)?(\d+)\.\s+(?:\[( |✔|✓|x)\]\s*)?(.+)$/;
59
+ const options = [];
60
+ let hasSelector = false;
61
+ let multi = false;
62
+ let firstOptionLine = -1;
63
+ let current = null;
64
+ for (let i = 0; i < lines.length; i++) {
65
+ const m = lines[i].match(optionRe);
66
+ if (m) {
67
+ if (m[1])
68
+ hasSelector = true;
69
+ current = { n: Number(m[2]), label: m[4].trim(), hint: "" };
70
+ if (m[3] !== undefined) {
71
+ multi = true;
72
+ current.checked = m[3] !== " ";
73
+ }
74
+ options.push(current);
75
+ if (firstOptionLine === -1)
76
+ firstOptionLine = i;
77
+ }
78
+ else if (current &&
79
+ /^\s{2,}\S/.test(lines[i]) &&
80
+ !/Enter to /i.test(lines[i]) &&
81
+ !/^\s*Submit\s*$/.test(lines[i])) {
82
+ // Indented line under an option: the option's description.
83
+ current.hint = (current.hint ? current.hint + " " : "") + lines[i].trim();
84
+ }
85
+ else if (lines[i].trim() !== "" && !/^\s*─+/.test(lines[i])) {
86
+ current = null;
87
+ }
88
+ }
89
+ if (options.length < 2 || !hasSelector)
90
+ return null;
91
+ // The question: the text lines right above the first option (skipping
92
+ // frames, separators and the "← ☐ … ✔ Submit →" tab bar).
93
+ const questionLines = [];
94
+ for (let i = firstOptionLine - 1; i >= 0; i--) {
95
+ const t = lines[i].trim();
96
+ if (t === "" || /^[─═╭╮╰╯│□⏺←→]/.test(t) || /[☐☒]|✔\s*Submit/.test(t)) {
97
+ if (questionLines.length)
98
+ break;
99
+ continue;
100
+ }
101
+ questionLines.unshift(t);
102
+ }
103
+ return { question: questionLines.join(" "), options, multi };
104
+ }
105
+ /**
106
+ * Reads a session transcript back from its .jsonl file
107
+ * (~/.claude/projects/<encoded cwd>/<session-id>.jsonl) so the history can
108
+ * be replayed when resuming the session.
109
+ */
110
+ export function loadHistory(cwd, sessionId) {
111
+ // Same drift-immunity as the tail: prefer the newest <id>.jsonl anywhere, so a
112
+ // moved/renamed worktree's history still resolves (see sessionFilePath).
113
+ const encoded = path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
114
+ const file = newestTranscriptById(sessionId) ??
115
+ path.join(os.homedir(), ".claude", "projects", encoded, sessionId + ".jsonl");
116
+ let raw;
117
+ try {
118
+ raw = fs.readFileSync(file, "utf8");
119
+ }
120
+ catch {
121
+ return [];
122
+ }
123
+ const turns = [];
124
+ for (const line of raw.split("\n")) {
125
+ if (!line.trim())
126
+ continue;
127
+ let e;
128
+ try {
129
+ e = JSON.parse(line);
130
+ }
131
+ catch {
132
+ continue;
133
+ }
134
+ if (e.isMeta || !e.message)
135
+ continue;
136
+ if (e.type === "user") {
137
+ const c = e.message.content;
138
+ let text = "";
139
+ if (typeof c === "string")
140
+ text = c;
141
+ else if (Array.isArray(c))
142
+ text = c
143
+ .filter((b) => b.type === "text")
144
+ .map((b) => b.text)
145
+ .join("\n");
146
+ text = text.trim();
147
+ // Skip technical messages (system reminders, commands, tool results,
148
+ // interruptions).
149
+ if (!text || text.startsWith("<") || text.startsWith("[Request interrupted"))
150
+ continue;
151
+ turns.push({ role: "user", text });
152
+ }
153
+ else if (e.type === "assistant" && Array.isArray(e.message.content)) {
154
+ const text = e.message.content
155
+ .filter((b) => b.type === "text")
156
+ .map((b) => b.text)
157
+ .join("\n")
158
+ .trim();
159
+ if (!text)
160
+ continue;
161
+ const last = turns[turns.length - 1];
162
+ if (last && last.role === "assistant")
163
+ last.text += "\n\n" + text;
164
+ else
165
+ turns.push({ role: "assistant", text });
166
+ }
167
+ }
168
+ return turns.slice(-100);
169
+ }
170
+ /**
171
+ * Lists the resumable sessions of a directory (newest first), with the
172
+ * first user prompt as a preview so a session can be recognized by more
173
+ * than its id.
174
+ */
175
+ export function listSessions(cwd) {
176
+ const encoded = path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
177
+ const dir = path.join(os.homedir(), ".claude", "projects", encoded);
178
+ let files;
179
+ try {
180
+ files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
181
+ }
182
+ catch {
183
+ return [];
184
+ }
185
+ const sessions = [];
186
+ for (const f of files) {
187
+ const file = path.join(dir, f);
188
+ let mtime;
189
+ try {
190
+ mtime = fs.statSync(file).mtimeMs;
191
+ }
192
+ catch {
193
+ continue;
194
+ }
195
+ sessions.push({
196
+ id: f.replace(/\.jsonl$/, ""),
197
+ mtime,
198
+ preview: firstUserPrompt(file),
199
+ });
200
+ }
201
+ return sessions.sort((a, b) => b.mtime - a.mtime);
202
+ }
203
+ /** Reads the beginning of a transcript and returns the first real user prompt. */
204
+ function firstUserPrompt(file) {
205
+ // Only the head of the file is needed — transcripts can be several MB.
206
+ let head;
207
+ try {
208
+ const fd = fs.openSync(file, "r");
209
+ try {
210
+ const buf = Buffer.alloc(256 * 1024);
211
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
212
+ head = buf.toString("utf8", 0, n);
213
+ }
214
+ finally {
215
+ fs.closeSync(fd);
216
+ }
217
+ }
218
+ catch {
219
+ return "";
220
+ }
221
+ for (const line of head.split("\n")) {
222
+ if (!line.trim())
223
+ continue;
224
+ let e;
225
+ try {
226
+ e = JSON.parse(line);
227
+ }
228
+ catch {
229
+ continue;
230
+ }
231
+ if (e.isMeta || e.type !== "user" || !e.message)
232
+ continue;
233
+ const c = e.message.content;
234
+ let text = "";
235
+ if (typeof c === "string")
236
+ text = c;
237
+ else if (Array.isArray(c))
238
+ text = c
239
+ .filter((b) => b.type === "text")
240
+ .map((b) => b.text)
241
+ .join("\n");
242
+ text = text.trim();
243
+ // Same filtering as loadHistory: skip technical messages.
244
+ if (!text || text.startsWith("<") || text.startsWith("[Request interrupted"))
245
+ continue;
246
+ return text.replace(/\s+/g, " ").slice(0, 120);
247
+ }
248
+ return "";
249
+ }
250
+ /**
251
+ * Finds the id of the most recent session of a directory: Claude Code
252
+ * writes each session to ~/.claude/projects/<encoded cwd>/<session-id>.jsonl.
253
+ */
254
+ export function findSessionId(cwd) {
255
+ const encoded = path.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
256
+ const dir = path.join(os.homedir(), ".claude", "projects", encoded);
257
+ try {
258
+ const newest = fs
259
+ .readdirSync(dir)
260
+ .filter((f) => f.endsWith(".jsonl"))
261
+ .map((f) => ({ f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
262
+ .sort((a, b) => b.mtime - a.mtime)[0];
263
+ return newest ? newest.f.replace(/\.jsonl$/, "") : null;
264
+ }
265
+ catch {
266
+ return null;
267
+ }
268
+ }
269
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"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"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { parseArgs } from "./args.js";
7
+ import { effectiveToken, loadConfig, migrateLegacyEnv, saveConfig } from "./config.js";
8
+ import { promptToken } from "./setup-prompt.js";
9
+ import { runSupervisor } from "./supervisor.js";
10
+ import { serverEntry, update } from "./updater.js";
11
+ import { writeUpdateResult } from "./update-flag.js";
12
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
13
+ function ownVersion() {
14
+ try {
15
+ return JSON.parse(fs.readFileSync(path.join(HERE, "..", "package.json"), "utf8")).version ?? "0.0.0";
16
+ }
17
+ catch {
18
+ return "0.0.0";
19
+ }
20
+ }
21
+ /** The server to run: the managed (updatable) install if present, else the
22
+ * server bundled next to this file. */
23
+ function serverToRun() {
24
+ const managed = serverEntry();
25
+ return fs.existsSync(managed) ? managed : path.join(HERE, "server.js");
26
+ }
27
+ async function main() {
28
+ const args = parseArgs(process.argv.slice(2));
29
+ if (args.help) {
30
+ process.stdout.write("shadok-ai — web cockpit for parallel Claude Code sessions\n\n" +
31
+ "Usage: npx shadok-ai [options]\n" +
32
+ " --port, -p <n> HTTP/WS port (default 3789)\n" +
33
+ " --no-telegram run web-only; don't prompt for or use a bot token\n" +
34
+ " --version, -v print version\n" +
35
+ " --help, -h this help\n\n" +
36
+ "Open http://localhost:<port>. In your Telegram board group, /setup to bind it.\n");
37
+ return 0;
38
+ }
39
+ if (args.version) {
40
+ process.stdout.write(ownVersion() + "\n");
41
+ return 0;
42
+ }
43
+ const cfg = loadConfig();
44
+ migrateLegacyEnv(cfg);
45
+ // First-run token prompt: only when we've never asked, it's interactive, and
46
+ // Telegram isn't disabled. A skip is recorded (null) so we never nag again.
47
+ if (!args.noTelegram &&
48
+ cfg.telegramToken === undefined &&
49
+ !process.env.TELEGRAM_BOT_TOKEN &&
50
+ process.stdin.isTTY) {
51
+ cfg.telegramToken = await promptToken();
52
+ saveConfig(cfg);
53
+ }
54
+ const port = args.port ?? cfg.port ?? (Number(process.env.PORT) || 3789);
55
+ const token = args.noTelegram ? null : effectiveToken(cfg);
56
+ const childEnv = { ...process.env, PORT: String(port) };
57
+ if (token)
58
+ childEnv.TELEGRAM_BOT_TOKEN = token;
59
+ else
60
+ delete childEnv.TELEGRAM_BOT_TOKEN;
61
+ let current = null;
62
+ // Ctrl-C / SIGTERM: bring the child down with us instead of orphaning it.
63
+ let shuttingDown = false;
64
+ for (const sig of ["SIGINT", "SIGTERM"]) {
65
+ process.on(sig, () => {
66
+ shuttingDown = true;
67
+ current?.kill(sig);
68
+ process.exit(0);
69
+ });
70
+ }
71
+ const deps = {
72
+ spawnServer: () => new Promise((resolve) => {
73
+ const child = spawn(process.execPath, [serverToRun()], { env: childEnv, stdio: "inherit" });
74
+ current = child;
75
+ child.on("exit", (code) => resolve(shuttingDown ? 0 : code ?? 0));
76
+ child.on("error", () => resolve(1));
77
+ }),
78
+ update,
79
+ writeUpdateResult,
80
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
81
+ now: () => Date.now(),
82
+ log: (m) => process.stdout.write(`[shadok-ai] ${m}\n`),
83
+ };
84
+ return runSupervisor(deps);
85
+ }
86
+ main().then((code) => process.exit(code), (e) => {
87
+ process.stderr.write(`[shadok-ai] fatal: ${e?.message ?? e}\n`);
88
+ process.exit(1);
89
+ });
90
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACvF,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAuB,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,SAAS,UAAU;IACjB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC;IACvG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED;wCACwC;AACxC,SAAS,WAAW;IAClB,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAC9B,OAAO,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;AACzE,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;QACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,+DAA+D;YAC7D,kCAAkC;YAClC,kDAAkD;YAClD,wEAAwE;YACxE,oCAAoC;YACpC,kCAAkC;YAClC,kFAAkF,CACrF,CAAC;QACF,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAEtB,6EAA6E;IAC7E,4EAA4E;IAC5E,IACE,CAAC,IAAI,CAAC,UAAU;QAChB,GAAG,CAAC,aAAa,KAAK,SAAS;QAC/B,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB;QAC/B,OAAO,CAAC,KAAK,CAAC,KAAK,EACnB,CAAC;QACD,GAAG,CAAC,aAAa,GAAG,MAAM,WAAW,EAAE,CAAC;QACxC,UAAU,CAAC,GAAG,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC;IACzE,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;IAE3D,MAAM,QAAQ,GAAsB,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;IAC3E,IAAI,KAAK;QAAE,QAAQ,CAAC,kBAAkB,GAAG,KAAK,CAAC;;QAC1C,OAAO,QAAQ,CAAC,kBAAkB,CAAC;IAExC,IAAI,OAAO,GAAoC,IAAI,CAAC;IACpD,0EAA0E;IAC1E,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,KAAK,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAU,EAAE,CAAC;QACjD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE;YACnB,YAAY,GAAG,IAAI,CAAC;YACpB,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,IAAI,GAAmB;QAC3B,WAAW,EAAE,GAAG,EAAE,CAChB,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;YAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;YAC5F,OAAO,GAAG,KAAK,CAAC;YAChB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;YAClE,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACtC,CAAC,CAAC;QACJ,MAAM;QACN,iBAAiB;QACjB,KAAK,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;QACrB,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC;KACvD,CAAC;IAEF,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC;AAED,IAAI,EAAE,CAAC,IAAI,CACT,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAC5B,CAAC,CAAC,EAAE,EAAE;IACJ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,EAAE,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CACF,CAAC"}
package/dist/pace.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { Usage, Window } from "./usage.js";
2
+ /** Durée totale de chaque fenêtre glissante, en secondes. */
3
+ export declare const WINDOW_SEC: {
4
+ readonly fiveHour: number;
5
+ readonly sevenDay: number;
6
+ };
7
+ /**
8
+ * Ajouté au rythme idéal au dénominateur. Sans lui, le rythme est quasi nul
9
+ * juste après un reset et le moindre message ferait exploser le ratio.
10
+ */
11
+ export declare const PACE_EPSILON = 2;
12
+ export interface Pace {
13
+ /** 0–100 : fraction de la fenêtre déjà écoulée. null si non calculable. */
14
+ idealPacePct: number | null;
15
+ /** Consommation rapportée au rythme idéal, en %. 100 = pile dans les temps. */
16
+ ratioPct: number | null;
17
+ }
18
+ export interface PaceVerdict {
19
+ blocked: boolean;
20
+ reason: string | null;
21
+ }
22
+ /**
23
+ * Rapporte la consommation d'une fenêtre au temps qui y est déjà passé.
24
+ * Renvoie des null si la fenêtre est absente ou son reset inconnu — pas de
25
+ * données ne vaut pas dépassement.
26
+ */
27
+ export declare function computePace(w: Window | null, durationSec: number, nowMs: number): Pace;
28
+ /**
29
+ * Bloque dès qu'UNE des deux fenêtres consomme plus vite que le temps ne passe.
30
+ * La raison cite la fenêtre au ratio le plus élevé. Sans données, ne bloque pas :
31
+ * l'indisponibilité de l'API ne doit pas verrouiller l'outil.
32
+ */
33
+ export declare function paceBlock(u: Usage | null, nowMs: number): PaceVerdict;
package/dist/pace.js ADDED
@@ -0,0 +1,53 @@
1
+ /** Durée totale de chaque fenêtre glissante, en secondes. */
2
+ export const WINDOW_SEC = { fiveHour: 5 * 3600, sevenDay: 7 * 86400 };
3
+ /**
4
+ * Ajouté au rythme idéal au dénominateur. Sans lui, le rythme est quasi nul
5
+ * juste après un reset et le moindre message ferait exploser le ratio.
6
+ */
7
+ export const PACE_EPSILON = 2;
8
+ /** Au-delà de ce ratio (en % du rythme idéal), on bloque. En dur, par choix. */
9
+ const BLOCK_RATIO = 100;
10
+ const LABEL = { fiveHour: "5h", sevenDay: "7d" };
11
+ /**
12
+ * Rapporte la consommation d'une fenêtre au temps qui y est déjà passé.
13
+ * Renvoie des null si la fenêtre est absente ou son reset inconnu — pas de
14
+ * données ne vaut pas dépassement.
15
+ */
16
+ export function computePace(w, durationSec, nowMs) {
17
+ if (!w || w.resetsAt === null)
18
+ return { idealPacePct: null, ratioPct: null };
19
+ const remainingSec = w.resetsAt - nowMs / 1000;
20
+ // Borné : une horloge en avance sur resetsAt ne doit pas produire de rythme
21
+ // négatif, ni une fenêtre expirée un rythme supérieur à 100%.
22
+ const idealPacePct = Math.min(100, Math.max(0, ((durationSec - remainingSec) / durationSec) * 100));
23
+ return { idealPacePct, ratioPct: (w.usedPercentage / (idealPacePct + PACE_EPSILON)) * 100 };
24
+ }
25
+ /**
26
+ * Bloque dès qu'UNE des deux fenêtres consomme plus vite que le temps ne passe.
27
+ * La raison cite la fenêtre au ratio le plus élevé. Sans données, ne bloque pas :
28
+ * l'indisponibilité de l'API ne doit pas verrouiller l'outil.
29
+ */
30
+ export function paceBlock(u, nowMs) {
31
+ if (!u)
32
+ return { blocked: false, reason: null };
33
+ let worst = null;
34
+ for (const key of ["fiveHour", "sevenDay"]) {
35
+ const w = u[key];
36
+ const { idealPacePct, ratioPct } = computePace(w, WINDOW_SEC[key], nowMs);
37
+ if (!w || idealPacePct === null || ratioPct === null)
38
+ continue;
39
+ if (ratioPct <= BLOCK_RATIO)
40
+ continue;
41
+ if (!worst || ratioPct > worst.ratio) {
42
+ worst = { label: LABEL[key], used: w.usedPercentage, pace: idealPacePct, ratio: ratioPct };
43
+ }
44
+ }
45
+ if (!worst)
46
+ return { blocked: false, reason: null };
47
+ const r = Math.round;
48
+ return {
49
+ blocked: true,
50
+ reason: `${worst.label}: ${r(worst.used)}% used vs ${r(worst.pace)}% ideal pace (${r(worst.ratio)}% of pace)`,
51
+ };
52
+ }
53
+ //# sourceMappingURL=pace.js.map