@gajae-code/coding-agent 0.6.4 → 0.7.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/CHANGELOG.md +51 -0
- package/dist/types/async/job-manager.d.ts +3 -1
- package/dist/types/cli/daemon-cli.d.ts +25 -0
- package/dist/types/cli/migrate-cli.d.ts +20 -0
- package/dist/types/cli/notify-cli.d.ts +23 -0
- package/dist/types/cli/setup-cli.d.ts +20 -1
- package/dist/types/commands/daemon.d.ts +41 -0
- package/dist/types/commands/migrate.d.ts +33 -0
- package/dist/types/commands/notify.d.ts +41 -0
- package/dist/types/config/keybindings.d.ts +4 -0
- package/dist/types/config/model-profile-activation.d.ts +12 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +38 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/daemon/builtin.d.ts +20 -0
- package/dist/types/daemon/control-types.d.ts +57 -0
- package/dist/types/daemon/runtime.d.ts +25 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -2
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +1 -1
- package/dist/types/gjc-runtime/session-layout.d.ts +59 -0
- package/dist/types/gjc-runtime/session-resolution.d.ts +47 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +1 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +5 -4
- package/dist/types/gjc-runtime/state-schema.d.ts +2 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +38 -7
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +15 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +1 -1
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +1 -1
- package/dist/types/harness-control-plane/storage.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +12 -4
- package/dist/types/migrate/action-planner.d.ts +11 -0
- package/dist/types/migrate/adapters/claude-code.d.ts +2 -0
- package/dist/types/migrate/adapters/codex.d.ts +5 -0
- package/dist/types/migrate/adapters/index.d.ts +45 -0
- package/dist/types/migrate/adapters/opencode.d.ts +2 -0
- package/dist/types/migrate/executor.d.ts +2 -0
- package/dist/types/migrate/mcp-mapper.d.ts +20 -0
- package/dist/types/migrate/report.d.ts +18 -0
- package/dist/types/migrate/skill-normalizer.d.ts +27 -0
- package/dist/types/migrate/types.d.ts +126 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/oauth-selector.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -2
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/notifications/config-commands.d.ts +26 -0
- package/dist/types/notifications/config.d.ts +61 -0
- package/dist/types/notifications/helpers.d.ts +55 -0
- package/dist/types/notifications/html-format.d.ts +62 -0
- package/dist/types/notifications/index.d.ts +28 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +93 -0
- package/dist/types/notifications/telegram-cli.d.ts +19 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/notifications/telegram-daemon-control.d.ts +56 -0
- package/dist/types/notifications/telegram-daemon.d.ts +276 -0
- package/dist/types/notifications/telegram-reference.d.ts +111 -0
- package/dist/types/notifications/threaded-inbound.d.ts +58 -0
- package/dist/types/notifications/threaded-render.d.ts +66 -0
- package/dist/types/notifications/topic-registry.d.ts +67 -0
- package/dist/types/research-plan/index.d.ts +1 -0
- package/dist/types/research-plan/ledger.d.ts +33 -0
- package/dist/types/rlm/artifacts.d.ts +1 -1
- package/dist/types/rlm/index.d.ts +12 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +26 -0
- package/dist/types/session/agent-session.d.ts +39 -2
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/setup/credential-auto-import.d.ts +63 -0
- package/dist/types/setup/credential-import.d.ts +3 -0
- package/dist/types/setup/host-plugin-setup.d.ts +39 -0
- package/dist/types/skill-state/active-state.d.ts +6 -11
- package/dist/types/skill-state/canonical-skills.d.ts +3 -0
- package/dist/types/skill-state/workflow-hud.d.ts +2 -0
- package/dist/types/task/spawn-gate.d.ts +1 -10
- package/dist/types/tools/ask-answer-registry.d.ts +13 -0
- package/dist/types/tools/index.d.ts +18 -0
- package/dist/types/tools/subagent.d.ts +3 -0
- package/package.json +7 -7
- package/scripts/build-binary.ts +3 -0
- package/src/async/job-manager.ts +5 -1
- package/src/cli/daemon-cli.ts +122 -0
- package/src/cli/migrate-cli.ts +106 -0
- package/src/cli/notify-cli.ts +274 -0
- package/src/cli/setup-cli.ts +173 -84
- package/src/cli.ts +3 -0
- package/src/commands/daemon.ts +47 -0
- package/src/commands/deep-interview.ts +2 -2
- package/src/commands/migrate.ts +46 -0
- package/src/commands/notify.ts +61 -0
- package/src/commands/setup.ts +11 -1
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +7 -3
- package/src/config/model-profile-activation.ts +74 -5
- package/src/config/model-profiles.ts +7 -4
- package/src/config/model-registry.ts +6 -3
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +29 -0
- package/src/coordinator/contract.ts +3 -0
- package/src/coordinator-mcp/policy.ts +10 -2
- package/src/coordinator-mcp/server.ts +270 -1
- package/src/daemon/builtin.ts +46 -0
- package/src/daemon/control-types.ts +65 -0
- package/src/daemon/runtime.ts +51 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +0 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +28 -24
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +51 -47
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +33 -13
- package/src/extensibility/custom-commands/loader.ts +0 -7
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/gjc-plugins/injection.ts +23 -4
- package/src/extensibility/gjc-plugins/state.ts +16 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +51 -18
- package/src/gjc-runtime/deep-interview-runtime.ts +49 -23
- package/src/gjc-runtime/goal-mode-request.ts +26 -11
- package/src/gjc-runtime/launch-tmux.ts +6 -1
- package/src/gjc-runtime/ralplan-runtime.ts +79 -50
- package/src/gjc-runtime/session-layout.ts +180 -0
- package/src/gjc-runtime/session-resolution.ts +217 -0
- package/src/gjc-runtime/state-graph.ts +1 -2
- package/src/gjc-runtime/state-migrations.ts +1 -0
- package/src/gjc-runtime/state-runtime.ts +247 -124
- package/src/gjc-runtime/state-schema.ts +2 -0
- package/src/gjc-runtime/state-writer.ts +289 -41
- package/src/gjc-runtime/team-runtime.ts +43 -19
- package/src/gjc-runtime/tmux-sessions.ts +7 -1
- package/src/gjc-runtime/ultragoal-guard.ts +102 -4
- package/src/gjc-runtime/ultragoal-runtime.ts +226 -60
- package/src/gjc-runtime/workflow-command-ref.ts +1 -2
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -2
- package/src/gjc-runtime/workflow-manifest.ts +12 -3
- package/src/goals/tools/goal-tool.ts +11 -2
- package/src/harness-control-plane/storage.ts +14 -4
- package/src/hooks/native-skill-hook.ts +38 -12
- package/src/hooks/skill-state.ts +178 -83
- package/src/internal-urls/docs-index.generated.ts +9 -6
- package/src/main.ts +30 -0
- package/src/migrate/action-planner.ts +318 -0
- package/src/migrate/adapters/claude-code.ts +39 -0
- package/src/migrate/adapters/codex.ts +70 -0
- package/src/migrate/adapters/index.ts +277 -0
- package/src/migrate/adapters/opencode.ts +52 -0
- package/src/migrate/executor.ts +81 -0
- package/src/migrate/mcp-mapper.ts +152 -0
- package/src/migrate/report.ts +104 -0
- package/src/migrate/skill-normalizer.ts +80 -0
- package/src/migrate/types.ts +163 -0
- package/src/modes/acp/acp-event-mapper.ts +1 -0
- package/src/modes/bridge/bridge-mode.ts +2 -2
- package/src/modes/components/custom-editor.ts +30 -20
- package/src/modes/components/hook-editor.ts +7 -2
- package/src/modes/components/oauth-selector.ts +19 -0
- package/src/modes/controllers/event-controller.ts +20 -0
- package/src/modes/controllers/selector-controller.ts +80 -17
- package/src/modes/interactive-mode.ts +6 -2
- package/src/modes/rpc/rpc-mode.ts +2 -2
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/event-contract.ts +1 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +1 -0
- package/src/modes/shared/agent-wire/event-observation.ts +16 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +3 -2
- package/src/modes/shared/agent-wire/unattended-session.ts +22 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/utils/ui-helpers.ts +23 -0
- package/src/notifications/config-commands.ts +50 -0
- package/src/notifications/config.ts +107 -0
- package/src/notifications/helpers.ts +135 -0
- package/src/notifications/html-format.ts +389 -0
- package/src/notifications/index.ts +663 -0
- package/src/notifications/rate-limit-pool.ts +179 -0
- package/src/notifications/telegram-cli.ts +194 -0
- package/src/notifications/telegram-daemon-cli.ts +74 -0
- package/src/notifications/telegram-daemon-control.ts +370 -0
- package/src/notifications/telegram-daemon.ts +1370 -0
- package/src/notifications/telegram-reference.ts +335 -0
- package/src/notifications/threaded-inbound.ts +80 -0
- package/src/notifications/threaded-render.ts +155 -0
- package/src/notifications/topic-registry.ts +133 -0
- package/src/prompts/agents/init.md +1 -1
- package/src/prompts/system/plan-mode-active.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/task.md +1 -2
- package/src/research-plan/index.ts +1 -0
- package/src/research-plan/ledger.ts +177 -0
- package/src/rlm/artifacts.ts +12 -3
- package/src/rlm/index.ts +26 -0
- package/src/runtime-mcp/config-writer.ts +46 -0
- package/src/sdk.ts +16 -0
- package/src/session/agent-session.ts +128 -24
- package/src/session/auth-storage.ts +3 -0
- package/src/session/session-dump-format.ts +43 -2
- package/src/session/session-manager.ts +39 -5
- package/src/setup/credential-auto-import.ts +258 -0
- package/src/setup/credential-import.ts +17 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +10 -0
- package/src/setup/hermes-setup.ts +1 -1
- package/src/setup/host-plugin-setup.ts +142 -0
- package/src/skill-state/active-state.ts +72 -108
- package/src/skill-state/canonical-skills.ts +4 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +28 -109
- package/src/skill-state/workflow-hud.ts +4 -2
- package/src/skill-state/workflow-state-contract.ts +3 -3
- package/src/slash-commands/builtin-registry.ts +4 -1
- package/src/task/agents.ts +1 -22
- package/src/task/executor.ts +5 -1
- package/src/task/index.ts +1 -41
- package/src/task/spawn-gate.ts +1 -38
- package/src/task/types.ts +1 -1
- package/src/tools/ask-answer-registry.ts +25 -0
- package/src/tools/ask.ts +108 -16
- package/src/tools/computer.ts +58 -4
- package/src/tools/image-gen.ts +5 -8
- package/src/tools/index.ts +19 -0
- package/src/tools/inspect-image.ts +16 -11
- package/src/tools/subagent-render.ts +7 -0
- package/src/tools/subagent.ts +38 -7
- package/dist/types/extensibility/custom-commands/bundled/review/index.d.ts +0 -10
- package/src/extensibility/custom-commands/bundled/review/index.ts +0 -456
- package/src/prompts/agents/explore.md +0 -58
- package/src/prompts/agents/plan.md +0 -49
- package/src/prompts/agents/reviewer.md +0 -141
- package/src/prompts/agents/task.md +0 -16
- package/src/prompts/review-request.md +0 -70
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-wide shared Telegram rate-limit pool for the threaded session surface.
|
|
3
|
+
*
|
|
4
|
+
* Multiple GJC sessions on one host share a single bot token and paired chat.
|
|
5
|
+
* Telegram enforces per-bot/per-chat limits (~1 message/sec, bursts up to ~20),
|
|
6
|
+
* so the singleton notifications daemon owns ONE pool that all per-session
|
|
7
|
+
* threads draw from. The pool provides:
|
|
8
|
+
*
|
|
9
|
+
* - a token bucket (burst capacity + steady refill) modelling the chat limit;
|
|
10
|
+
* - priority lanes (`ask` > `finalized` > `live` > `idle`) so urgent frames
|
|
11
|
+
* win scarce tokens;
|
|
12
|
+
* - per-session round-robin fairness within a lane so one session's live-edit
|
|
13
|
+
* stream cannot starve other sessions;
|
|
14
|
+
* - coalescing of live edits that share a `coalesceKey` (the latest rendered
|
|
15
|
+
* text replaces the queued one) so throttled edit storms collapse.
|
|
16
|
+
*
|
|
17
|
+
* The core is a pull-based scheduler with an injectable clock so fairness,
|
|
18
|
+
* starvation, and burst behaviour are deterministically unit-testable without
|
|
19
|
+
* real time or a live Bot API.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Delivery lanes in descending priority. */
|
|
23
|
+
export type RateLimitLane = "ask" | "finalized" | "live" | "idle";
|
|
24
|
+
|
|
25
|
+
/** Lanes ordered from highest to lowest priority. */
|
|
26
|
+
export const LANE_PRIORITY: readonly RateLimitLane[] = ["ask", "finalized", "live", "idle"];
|
|
27
|
+
|
|
28
|
+
/** A unit of work competing for a send slot. */
|
|
29
|
+
export interface RateLimitItem<T = unknown> {
|
|
30
|
+
/** Owning session id (used for per-session fairness). */
|
|
31
|
+
sessionId: string;
|
|
32
|
+
/** Priority lane. */
|
|
33
|
+
lane: RateLimitLane;
|
|
34
|
+
/**
|
|
35
|
+
* Optional coalesce key. Submitting another item with the same
|
|
36
|
+
* `(sessionId, lane, coalesceKey)` replaces the queued payload with the
|
|
37
|
+
* newer one instead of enqueuing a duplicate (used for live edits).
|
|
38
|
+
*/
|
|
39
|
+
coalesceKey?: string;
|
|
40
|
+
/** Opaque payload the caller maps to an actual Telegram send. */
|
|
41
|
+
payload: T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Options for {@link RateLimitPool}. */
|
|
45
|
+
export interface RateLimitPoolOptions {
|
|
46
|
+
/** Burst capacity (max tokens). Default 20 (Telegram per-chat burst). */
|
|
47
|
+
capacity?: number;
|
|
48
|
+
/** Steady refill rate in tokens per second. Default 1 (~1 msg/sec/chat). */
|
|
49
|
+
refillPerSec?: number;
|
|
50
|
+
/** Injectable clock in ms. Default `Date.now`. */
|
|
51
|
+
now?: () => number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface QueuedItem<T> {
|
|
55
|
+
item: RateLimitItem<T>;
|
|
56
|
+
seq: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A deterministic, pull-based shared rate-limit scheduler.
|
|
61
|
+
*
|
|
62
|
+
* Callers {@link submit} work and periodically {@link drain} (e.g. on a timer
|
|
63
|
+
* or after each submit); `drain` returns the items granted a send slot, in the
|
|
64
|
+
* order they should be sent.
|
|
65
|
+
*/
|
|
66
|
+
export class RateLimitPool<T = unknown> {
|
|
67
|
+
private readonly capacity: number;
|
|
68
|
+
private readonly refillPerSec: number;
|
|
69
|
+
private readonly now: () => number;
|
|
70
|
+
|
|
71
|
+
/** Per-lane FIFO queues; each lane holds items across sessions. */
|
|
72
|
+
private readonly lanes = new Map<RateLimitLane, QueuedItem<T>[]>();
|
|
73
|
+
/** Rotating session cursor per lane for round-robin fairness. */
|
|
74
|
+
private readonly laneCursor = new Map<RateLimitLane, number>();
|
|
75
|
+
|
|
76
|
+
private tokens: number;
|
|
77
|
+
private lastRefill: number;
|
|
78
|
+
private seqCounter = 0;
|
|
79
|
+
|
|
80
|
+
constructor(options: RateLimitPoolOptions = {}) {
|
|
81
|
+
this.capacity = Math.max(1, options.capacity ?? 20);
|
|
82
|
+
this.refillPerSec = Math.max(0, options.refillPerSec ?? 1);
|
|
83
|
+
this.now = options.now ?? Date.now;
|
|
84
|
+
this.tokens = this.capacity;
|
|
85
|
+
this.lastRefill = this.now();
|
|
86
|
+
for (const lane of LANE_PRIORITY) this.lanes.set(lane, []);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Number of items currently queued across all lanes. */
|
|
90
|
+
get pending(): number {
|
|
91
|
+
let total = 0;
|
|
92
|
+
for (const queue of this.lanes.values()) total += queue.length;
|
|
93
|
+
return total;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Current available token count (after refill at `now`). */
|
|
97
|
+
availableTokens(nowMs: number = this.now()): number {
|
|
98
|
+
this.refill(nowMs);
|
|
99
|
+
return this.tokens;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Submit an item. If it carries a `coalesceKey` matching a queued item in
|
|
104
|
+
* the same `(sessionId, lane)`, the queued payload is replaced (latest
|
|
105
|
+
* wins) and FIFO position is preserved; otherwise it is appended.
|
|
106
|
+
*/
|
|
107
|
+
submit(item: RateLimitItem<T>): void {
|
|
108
|
+
const queue = this.lanes.get(item.lane);
|
|
109
|
+
if (!queue) throw new Error(`unknown rate-limit lane: ${item.lane}`);
|
|
110
|
+
if (item.coalesceKey !== undefined) {
|
|
111
|
+
const existing = queue.find(
|
|
112
|
+
q => q.item.sessionId === item.sessionId && q.item.coalesceKey === item.coalesceKey,
|
|
113
|
+
);
|
|
114
|
+
if (existing) {
|
|
115
|
+
existing.item = item;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
queue.push({ item, seq: this.seqCounter++ });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Grant as many queued items as tokens allow at `nowMs`. Items are selected
|
|
124
|
+
* by lane priority, then round-robin across sessions within a lane (so no
|
|
125
|
+
* single session monopolises a lane), consuming one token each.
|
|
126
|
+
*/
|
|
127
|
+
drain(nowMs: number = this.now()): RateLimitItem<T>[] {
|
|
128
|
+
this.refill(nowMs);
|
|
129
|
+
const granted: RateLimitItem<T>[] = [];
|
|
130
|
+
while (this.tokens >= 1) {
|
|
131
|
+
const next = this.takeNext();
|
|
132
|
+
if (!next) break;
|
|
133
|
+
this.tokens -= 1;
|
|
134
|
+
granted.push(next);
|
|
135
|
+
}
|
|
136
|
+
return granted;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private refill(nowMs: number): void {
|
|
140
|
+
if (nowMs <= this.lastRefill) return;
|
|
141
|
+
const elapsedSec = (nowMs - this.lastRefill) / 1000;
|
|
142
|
+
this.tokens = Math.min(this.capacity, this.tokens + elapsedSec * this.refillPerSec);
|
|
143
|
+
this.lastRefill = nowMs;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Pop the next item by lane priority + per-session round-robin fairness. */
|
|
147
|
+
private takeNext(): RateLimitItem<T> | undefined {
|
|
148
|
+
for (const lane of LANE_PRIORITY) {
|
|
149
|
+
const queue = this.lanes.get(lane)!;
|
|
150
|
+
if (queue.length === 0) continue;
|
|
151
|
+
const picked = this.pickFairIndex(lane, queue);
|
|
152
|
+
const [removed] = queue.splice(picked, 1);
|
|
153
|
+
return removed?.item;
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Choose the index to serve from a lane queue using round-robin over the
|
|
160
|
+
* distinct session ids present, starting just after the last-served
|
|
161
|
+
* session. Falls back to FIFO (index 0) when only one session is queued.
|
|
162
|
+
*/
|
|
163
|
+
private pickFairIndex(lane: RateLimitLane, queue: QueuedItem<T>[]): number {
|
|
164
|
+
const sessions: string[] = [];
|
|
165
|
+
for (const q of queue) if (!sessions.includes(q.item.sessionId)) sessions.push(q.item.sessionId);
|
|
166
|
+
if (sessions.length <= 1) return 0;
|
|
167
|
+
const cursor = this.laneCursor.get(lane) ?? 0;
|
|
168
|
+
// Choose the earliest-queued item whose session is the next in rotation.
|
|
169
|
+
for (let offset = 0; offset < sessions.length; offset++) {
|
|
170
|
+
const candidate = sessions[(cursor + offset) % sessions.length]!;
|
|
171
|
+
const idx = queue.findIndex(q => q.item.sessionId === candidate);
|
|
172
|
+
if (idx >= 0) {
|
|
173
|
+
this.laneCursor.set(lane, (cursor + offset + 1) % sessions.length);
|
|
174
|
+
return idx;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Reference CLI for the notifications SDK Telegram client.
|
|
4
|
+
*
|
|
5
|
+
* Bridges a running GJC session's notification endpoint to a Telegram bot so you
|
|
6
|
+
* can answer asks / see idle pings from your phone — no RPC mode required. This
|
|
7
|
+
* is an EXAMPLE/template (the SDK contract is in `docs/notifications-sdk.md`);
|
|
8
|
+
* Discord/Slack clients are written the same way.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* bun run packages/coding-agent/src/notifications/telegram-cli.ts \
|
|
12
|
+
* --bot-token <token> [--chat-id <id>] [--endpoint-file <path> | --session-id <id>] [--repo <dir>]
|
|
13
|
+
*
|
|
14
|
+
* Env fallbacks: GJC_TG_BOT_TOKEN, GJC_TG_CHAT_ID.
|
|
15
|
+
* If --chat-id is omitted it is auto-resolved from getUpdates (message the bot once).
|
|
16
|
+
* If neither --endpoint-file nor --session-id is given, the newest endpoint file
|
|
17
|
+
* under <repo>/.gjc/state/notifications/ is used.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import * as fs from "node:fs";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { getAgentDir } from "@gajae-code/utils";
|
|
23
|
+
import { tokenFingerprint } from "./config";
|
|
24
|
+
import { type DaemonState, daemonPaths, isFreshLiveOwner } from "./telegram-daemon";
|
|
25
|
+
import { runTelegramReferenceClient } from "./telegram-reference";
|
|
26
|
+
|
|
27
|
+
interface CliArgs {
|
|
28
|
+
botToken?: string;
|
|
29
|
+
chatId?: string;
|
|
30
|
+
endpointFile?: string;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
repo: string;
|
|
33
|
+
apiBase?: string;
|
|
34
|
+
force: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv: string[]): CliArgs {
|
|
38
|
+
const args: CliArgs = { repo: process.cwd(), force: false };
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
const next = () => argv[++i];
|
|
41
|
+
switch (argv[i]) {
|
|
42
|
+
case "--bot-token":
|
|
43
|
+
args.botToken = next();
|
|
44
|
+
break;
|
|
45
|
+
case "--chat-id":
|
|
46
|
+
args.chatId = next();
|
|
47
|
+
break;
|
|
48
|
+
case "--endpoint-file":
|
|
49
|
+
args.endpointFile = next();
|
|
50
|
+
break;
|
|
51
|
+
case "--session-id":
|
|
52
|
+
args.sessionId = next();
|
|
53
|
+
break;
|
|
54
|
+
case "--repo":
|
|
55
|
+
args.repo = next() ?? process.cwd();
|
|
56
|
+
break;
|
|
57
|
+
case "--api-base":
|
|
58
|
+
args.apiBase = next();
|
|
59
|
+
break;
|
|
60
|
+
case "--force":
|
|
61
|
+
args.force = true;
|
|
62
|
+
break;
|
|
63
|
+
case "-h":
|
|
64
|
+
case "--help":
|
|
65
|
+
printHelpAndExit();
|
|
66
|
+
break;
|
|
67
|
+
default:
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return args;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function printHelpAndExit(): never {
|
|
75
|
+
process.stdout.write(
|
|
76
|
+
[
|
|
77
|
+
"gjc notifications — Telegram reference client",
|
|
78
|
+
"",
|
|
79
|
+
" --bot-token <token> Telegram bot token (or env GJC_TG_BOT_TOKEN)",
|
|
80
|
+
" --chat-id <id> Target chat id (or env GJC_TG_CHAT_ID; auto-resolved if omitted)",
|
|
81
|
+
" --endpoint-file <path> Session endpoint discovery file",
|
|
82
|
+
" --session-id <id> Resolve <repo>/.gjc/state/notifications/<id>.json",
|
|
83
|
+
" --repo <dir> Repo root for endpoint discovery (default: cwd)",
|
|
84
|
+
" --api-base <url> Telegram API base (default: https://api.telegram.org)",
|
|
85
|
+
" --force Bypass active daemon guard (debug only; may cause Telegram 409 conflicts)",
|
|
86
|
+
"",
|
|
87
|
+
].join("\n"),
|
|
88
|
+
);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Find the most recently modified endpoint discovery file under the repo. */
|
|
93
|
+
function findLatestEndpoint(repo: string): string | undefined {
|
|
94
|
+
const dir = path.join(repo, ".gjc", "state", "notifications");
|
|
95
|
+
let entries: string[];
|
|
96
|
+
try {
|
|
97
|
+
entries = fs.readdirSync(dir).filter(f => f.endsWith(".json"));
|
|
98
|
+
} catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
let best: { file: string; mtime: number } | undefined;
|
|
102
|
+
for (const f of entries) {
|
|
103
|
+
const full = path.join(dir, f);
|
|
104
|
+
const mtime = fs.statSync(full).mtimeMs;
|
|
105
|
+
if (!best || mtime > best.mtime) best = { file: full, mtime };
|
|
106
|
+
}
|
|
107
|
+
return best?.file;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function resolveChatId(botToken: string, apiBase: string): Promise<string> {
|
|
111
|
+
const api = `${apiBase}/bot${botToken}`;
|
|
112
|
+
for (let i = 0; i < 150; i++) {
|
|
113
|
+
const body = (await fetch(`${api}/getUpdates`)
|
|
114
|
+
.then(r => r.json())
|
|
115
|
+
.catch(() => ({}))) as {
|
|
116
|
+
result?: Array<Record<string, any>>;
|
|
117
|
+
};
|
|
118
|
+
for (const u of body.result ?? []) {
|
|
119
|
+
const id = u.message?.chat?.id ?? u.callback_query?.message?.chat?.id;
|
|
120
|
+
if (id !== undefined) return String(id);
|
|
121
|
+
}
|
|
122
|
+
if (i === 0) process.stderr.write("Waiting for a message to the bot to resolve the chat id...\n");
|
|
123
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
124
|
+
}
|
|
125
|
+
throw new Error("could not resolve a chat id; send the bot a message first");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function pidAlive(pid: number): boolean {
|
|
129
|
+
try {
|
|
130
|
+
process.kill(pid, 0);
|
|
131
|
+
return true;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readDaemonState(): DaemonState | undefined {
|
|
138
|
+
try {
|
|
139
|
+
const raw = fs.readFileSync(daemonPaths(getAgentDir()).state, "utf8");
|
|
140
|
+
return JSON.parse(raw) as DaemonState;
|
|
141
|
+
} catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function activeDaemonOwnsToken(input: { botToken: string; chatId: string }): boolean {
|
|
147
|
+
const state = readDaemonState();
|
|
148
|
+
if (!state) return false;
|
|
149
|
+
return isFreshLiveOwner({
|
|
150
|
+
state,
|
|
151
|
+
now: Date.now(),
|
|
152
|
+
tokenFingerprint: tokenFingerprint(input.botToken),
|
|
153
|
+
chatId: input.chatId,
|
|
154
|
+
pidAlive,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function main(): Promise<void> {
|
|
159
|
+
const args = parseArgs(process.argv.slice(2));
|
|
160
|
+
const apiBase = args.apiBase ?? "https://api.telegram.org";
|
|
161
|
+
const botToken = args.botToken ?? process.env.GJC_TG_BOT_TOKEN;
|
|
162
|
+
if (!botToken) {
|
|
163
|
+
process.stderr.write("error: --bot-token (or GJC_TG_BOT_TOKEN) is required\n");
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const endpointFile =
|
|
168
|
+
args.endpointFile ??
|
|
169
|
+
(args.sessionId ? path.join(args.repo, ".gjc", "state", "notifications", `${args.sessionId}.json`) : undefined) ??
|
|
170
|
+
findLatestEndpoint(args.repo);
|
|
171
|
+
if (!endpointFile || !fs.existsSync(endpointFile)) {
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
`error: no endpoint file found (looked under ${args.repo}/.gjc/state/notifications). Start a session with GJC_NOTIFICATIONS=1 first.\n`,
|
|
174
|
+
);
|
|
175
|
+
process.exit(2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const chatId = args.chatId ?? process.env.GJC_TG_CHAT_ID ?? (await resolveChatId(botToken, apiBase));
|
|
179
|
+
if (!args.force && activeDaemonOwnsToken({ botToken, chatId })) {
|
|
180
|
+
process.stderr.write(
|
|
181
|
+
"an active gjc notifications daemon already owns this bot token; running a second poller will cause Telegram 409 conflicts. Re-run with --force to override.\n",
|
|
182
|
+
);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
process.stderr.write(`notifications: bridging ${endpointFile} <-> Telegram chat ${chatId}\n`);
|
|
186
|
+
await runTelegramReferenceClient({ botToken, chatId, endpointFile, apiBase });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (import.meta.main) {
|
|
190
|
+
main().catch(e => {
|
|
191
|
+
process.stderr.write(`fatal: ${String(e)}\n`);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { Settings } from "../config/settings";
|
|
4
|
+
import { getNotificationConfig, isGloballyConfigured } from "./config";
|
|
5
|
+
import { daemonPaths, TelegramNotificationDaemon } from "./telegram-daemon";
|
|
6
|
+
import { clearTelegramControlRequest, readTelegramControlRequest } from "./telegram-daemon-control";
|
|
7
|
+
|
|
8
|
+
export interface RunDaemonInternalDeps {
|
|
9
|
+
SettingsImpl?: Pick<typeof Settings, "init">;
|
|
10
|
+
DaemonImpl?: typeof TelegramNotificationDaemon;
|
|
11
|
+
processPid?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function argValue(argv: string[], name: string): string | undefined {
|
|
15
|
+
const i = argv.indexOf(name);
|
|
16
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runDaemonSmoke(opts: { agentDir?: string } = {}): Promise<void> {
|
|
20
|
+
const agentDir = opts.agentDir ?? fs.mkdtempSync(path.join(process.cwd(), ".telegram-daemon-smoke-"));
|
|
21
|
+
const settings = Settings.isolated({});
|
|
22
|
+
const paths = daemonPaths(agentDir);
|
|
23
|
+
await fs.promises.mkdir(paths.dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
const tempLock = `${paths.lock}.smoke.${process.pid}`;
|
|
25
|
+
const handle = await fs.promises.open(tempLock, "wx", 0o600);
|
|
26
|
+
await handle.close();
|
|
27
|
+
await fs.promises.unlink(tempLock);
|
|
28
|
+
void settings;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalDeps = {}): Promise<void> {
|
|
32
|
+
const smoke = argv.includes("--smoke");
|
|
33
|
+
const agentDir = argValue(argv, "--agent-dir");
|
|
34
|
+
if (smoke) return runDaemonSmoke({ agentDir });
|
|
35
|
+
const ownerId = argValue(argv, "--owner-id");
|
|
36
|
+
if (!ownerId) throw new Error("missing --owner-id");
|
|
37
|
+
const settings = await (deps.SettingsImpl ?? Settings).init(agentDir ? { agentDir } : {});
|
|
38
|
+
const cfg = getNotificationConfig(settings);
|
|
39
|
+
if (!isGloballyConfigured(cfg) || !cfg.botToken || !cfg.chatId) return;
|
|
40
|
+
const Daemon = deps.DaemonImpl ?? TelegramNotificationDaemon;
|
|
41
|
+
const daemon = new Daemon({
|
|
42
|
+
settings,
|
|
43
|
+
ownerId,
|
|
44
|
+
botToken: cfg.botToken,
|
|
45
|
+
chatId: cfg.chatId,
|
|
46
|
+
idleTimeoutMs: cfg.idleTimeoutMs,
|
|
47
|
+
pid: deps.processPid ?? process.pid,
|
|
48
|
+
control: {
|
|
49
|
+
shouldStop: async owner => {
|
|
50
|
+
const req = await readTelegramControlRequest(settings);
|
|
51
|
+
return Boolean(req && (!req.ownerId || req.ownerId === owner));
|
|
52
|
+
},
|
|
53
|
+
clear: async owner => {
|
|
54
|
+
const req = await readTelegramControlRequest(settings);
|
|
55
|
+
// Only clear a request that targets this daemon owner, so an exiting
|
|
56
|
+
// daemon never erases a newer request meant for a different owner.
|
|
57
|
+
if (req && (!req.ownerId || req.ownerId === owner)) {
|
|
58
|
+
await clearTelegramControlRequest(settings, req.requestId);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
// Signals are a process concern: install them at the daemon-internal boundary,
|
|
64
|
+
// not inside the embeddable daemon class. SIGTERM is the reload wakeup path.
|
|
65
|
+
const onSignal = (): void => daemon.requestStop("signal");
|
|
66
|
+
process.once("SIGTERM", onSignal);
|
|
67
|
+
process.once("SIGINT", onSignal);
|
|
68
|
+
try {
|
|
69
|
+
await daemon.run();
|
|
70
|
+
} finally {
|
|
71
|
+
process.off("SIGTERM", onSignal);
|
|
72
|
+
process.off("SIGINT", onSignal);
|
|
73
|
+
}
|
|
74
|
+
}
|