@mininglamp-oss/cc-channel-octo 1.0.1-dev.60b73f3

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 (87) hide show
  1. package/CHANGELOG.md +349 -0
  2. package/LICENSE +191 -0
  3. package/README.md +577 -0
  4. package/config.bot.example.json +15 -0
  5. package/config.example.json +33 -0
  6. package/dist/agent-bridge.d.ts +79 -0
  7. package/dist/agent-bridge.js +392 -0
  8. package/dist/agent-bridge.js.map +1 -0
  9. package/dist/commands.d.ts +57 -0
  10. package/dist/commands.js +121 -0
  11. package/dist/commands.js.map +1 -0
  12. package/dist/config.d.ts +287 -0
  13. package/dist/config.js +332 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/cron-evaluator.d.ts +53 -0
  16. package/dist/cron-evaluator.js +191 -0
  17. package/dist/cron-evaluator.js.map +1 -0
  18. package/dist/cron-fire-marker.d.ts +24 -0
  19. package/dist/cron-fire-marker.js +25 -0
  20. package/dist/cron-fire-marker.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +46 -0
  22. package/dist/cron-scheduler.js +114 -0
  23. package/dist/cron-scheduler.js.map +1 -0
  24. package/dist/cron-store.d.ts +62 -0
  25. package/dist/cron-store.js +63 -0
  26. package/dist/cron-store.js.map +1 -0
  27. package/dist/cron-tool.d.ts +44 -0
  28. package/dist/cron-tool.js +151 -0
  29. package/dist/cron-tool.js.map +1 -0
  30. package/dist/cwd-resolver.d.ts +72 -0
  31. package/dist/cwd-resolver.js +166 -0
  32. package/dist/cwd-resolver.js.map +1 -0
  33. package/dist/db-adapter.d.ts +21 -0
  34. package/dist/db-adapter.js +64 -0
  35. package/dist/db-adapter.js.map +1 -0
  36. package/dist/file-inline-wrap.d.ts +94 -0
  37. package/dist/file-inline-wrap.js +243 -0
  38. package/dist/file-inline-wrap.js.map +1 -0
  39. package/dist/gateway.d.ts +100 -0
  40. package/dist/gateway.js +420 -0
  41. package/dist/gateway.js.map +1 -0
  42. package/dist/group-config.d.ts +41 -0
  43. package/dist/group-config.js +104 -0
  44. package/dist/group-config.js.map +1 -0
  45. package/dist/group-context.d.ts +81 -0
  46. package/dist/group-context.js +466 -0
  47. package/dist/group-context.js.map +1 -0
  48. package/dist/inbound.d.ts +136 -0
  49. package/dist/inbound.js +667 -0
  50. package/dist/inbound.js.map +1 -0
  51. package/dist/index.d.ts +33 -0
  52. package/dist/index.js +932 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/media-inbound.d.ts +38 -0
  55. package/dist/media-inbound.js +131 -0
  56. package/dist/media-inbound.js.map +1 -0
  57. package/dist/mention-utils.d.ts +108 -0
  58. package/dist/mention-utils.js +199 -0
  59. package/dist/mention-utils.js.map +1 -0
  60. package/dist/octo/api.d.ts +148 -0
  61. package/dist/octo/api.js +320 -0
  62. package/dist/octo/api.js.map +1 -0
  63. package/dist/octo/socket.d.ts +102 -0
  64. package/dist/octo/socket.js +793 -0
  65. package/dist/octo/socket.js.map +1 -0
  66. package/dist/octo/types.d.ts +126 -0
  67. package/dist/octo/types.js +35 -0
  68. package/dist/octo/types.js.map +1 -0
  69. package/dist/prompt-safety.d.ts +78 -0
  70. package/dist/prompt-safety.js +148 -0
  71. package/dist/prompt-safety.js.map +1 -0
  72. package/dist/session-router.d.ts +144 -0
  73. package/dist/session-router.js +490 -0
  74. package/dist/session-router.js.map +1 -0
  75. package/dist/session-store.d.ts +89 -0
  76. package/dist/session-store.js +297 -0
  77. package/dist/session-store.js.map +1 -0
  78. package/dist/skill-linker.d.ts +31 -0
  79. package/dist/skill-linker.js +160 -0
  80. package/dist/skill-linker.js.map +1 -0
  81. package/dist/stream-relay.d.ts +42 -0
  82. package/dist/stream-relay.js +243 -0
  83. package/dist/stream-relay.js.map +1 -0
  84. package/dist/url-policy.d.ts +103 -0
  85. package/dist/url-policy.js +290 -0
  86. package/dist/url-policy.js.map +1 -0
  87. package/package.json +79 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Session Store — SQLite persistence via better-sqlite3 + thin adapter.
3
+ */
4
+ import { escapeRoleLabels, sanitizeDisplayName } from './prompt-safety.js';
5
+ const SCHEMA = `
6
+ CREATE TABLE IF NOT EXISTS sessions (
7
+ id TEXT PRIMARY KEY,
8
+ channel_id TEXT NOT NULL,
9
+ channel_type INTEGER NOT NULL,
10
+ created_at INTEGER NOT NULL,
11
+ updated_at INTEGER NOT NULL
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS messages (
15
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
16
+ session_id TEXT NOT NULL,
17
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
18
+ content TEXT NOT NULL,
19
+ timestamp INTEGER NOT NULL,
20
+ message_seq INTEGER,
21
+ from_name TEXT,
22
+ FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS group_members (
26
+ group_id TEXT NOT NULL,
27
+ uid TEXT NOT NULL,
28
+ name TEXT NOT NULL,
29
+ updated_at INTEGER NOT NULL,
30
+ PRIMARY KEY(group_id, uid)
31
+ );
32
+
33
+ -- v0.3 /reset barrier: the message_seq at which a session was intentionally
34
+ -- cleared. Kept in a SEPARATE table (no FK to sessions) so it SURVIVES
35
+ -- deleteSession() and a process restart — G4 cold-start backfill consults it to
36
+ -- avoid resurrecting pre-reset history. One row per session that ever reset.
37
+ CREATE TABLE IF NOT EXISTS reset_barriers (
38
+ session_id TEXT PRIMARY KEY,
39
+ reset_seq INTEGER NOT NULL
40
+ );
41
+
42
+ -- v0.3 persistent sessions: maps our sessionKey to the SDK's session UUID so a
43
+ -- later turn can resume the same agent session (v2 Session API). Kept separate
44
+ -- from the sessions table (different lifecycle); cleared by /reset with history.
45
+ CREATE TABLE IF NOT EXISTS sdk_sessions (
46
+ session_id TEXT PRIMARY KEY,
47
+ sdk_session_id TEXT NOT NULL,
48
+ updated_at INTEGER NOT NULL
49
+ );
50
+
51
+ CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages(session_id, id);
52
+ `;
53
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
54
+ function rowToSession(row) {
55
+ return {
56
+ id: row.id,
57
+ channelId: row.channel_id,
58
+ channelType: row.channel_type,
59
+ createdAt: row.created_at,
60
+ updatedAt: row.updated_at,
61
+ };
62
+ }
63
+ export class SessionStore {
64
+ adapter;
65
+ selectSession;
66
+ insertSession;
67
+ touchSession;
68
+ insertMessage;
69
+ selectRecentMessages;
70
+ deleteExpired;
71
+ deleteSessionStmt;
72
+ upsertResetBarrier;
73
+ selectResetBarrier;
74
+ upsertSdkSession;
75
+ selectSdkSession;
76
+ deleteSdkSession;
77
+ deleteExpiredSdkSessions;
78
+ /** Tracks the last message_seq at which the bot replied, per group session key. */
79
+ lastBotReplySeq = new Map();
80
+ constructor(adapter) {
81
+ this.adapter = adapter;
82
+ }
83
+ init() {
84
+ this.adapter.exec(SCHEMA);
85
+ // Migrations: add columns missing on pre-existing DBs (SQLite can't add them
86
+ // via CREATE TABLE IF NOT EXISTS). Guarded + throw on real failure (Q1-2) so
87
+ // a silent failure can't surface later as cryptic SQL errors. Note: this is
88
+ // schema presence, NOT data back-compat. New rows always write from_name;
89
+ // older rows added before this column may be NULL, which renderTurn handles
90
+ // via `from_name ?? role` — keep that coalesce (it is not dead code).
91
+ try {
92
+ const cols = this.adapter
93
+ .prepare("PRAGMA table_info(messages)")
94
+ .all();
95
+ if (!cols.some((c) => c.name === 'message_seq')) {
96
+ this.adapter.exec('ALTER TABLE messages ADD COLUMN message_seq INTEGER');
97
+ }
98
+ if (!cols.some((c) => c.name === 'from_name')) {
99
+ this.adapter.exec('ALTER TABLE messages ADD COLUMN from_name TEXT');
100
+ }
101
+ }
102
+ catch (err) {
103
+ throw new Error(`session-store: messages column migration failed — database is in an unknown state. Underlying error: ${String(err)}`);
104
+ }
105
+ this.selectSession = this.adapter.prepare('SELECT * FROM sessions WHERE id = ?');
106
+ this.insertSession = this.adapter.prepare('INSERT INTO sessions (id, channel_id, channel_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)');
107
+ this.touchSession = this.adapter.prepare('UPDATE sessions SET updated_at = ? WHERE id = ?');
108
+ this.insertMessage = this.adapter.prepare('INSERT INTO messages (session_id, role, content, timestamp, message_seq, from_name) VALUES (?, ?, ?, ?, ?, ?)');
109
+ this.selectRecentMessages = this.adapter.prepare('SELECT role, content, message_seq, from_name FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?');
110
+ this.deleteExpired = this.adapter.prepare('DELETE FROM sessions WHERE updated_at < ?');
111
+ this.deleteSessionStmt = this.adapter.prepare('DELETE FROM sessions WHERE id = ?');
112
+ this.upsertResetBarrier = this.adapter.prepare('INSERT INTO reset_barriers (session_id, reset_seq) VALUES (?, ?) ' +
113
+ 'ON CONFLICT(session_id) DO UPDATE SET reset_seq = excluded.reset_seq ' +
114
+ 'WHERE excluded.reset_seq > reset_barriers.reset_seq');
115
+ this.selectResetBarrier = this.adapter.prepare('SELECT reset_seq FROM reset_barriers WHERE session_id = ?');
116
+ this.upsertSdkSession = this.adapter.prepare('INSERT INTO sdk_sessions (session_id, sdk_session_id, updated_at) VALUES (?, ?, ?) ' +
117
+ 'ON CONFLICT(session_id) DO UPDATE SET sdk_session_id = excluded.sdk_session_id, ' +
118
+ 'updated_at = excluded.updated_at');
119
+ this.selectSdkSession = this.adapter.prepare('SELECT sdk_session_id FROM sdk_sessions WHERE session_id = ?');
120
+ this.deleteSdkSession = this.adapter.prepare('DELETE FROM sdk_sessions WHERE session_id = ?');
121
+ this.deleteExpiredSdkSessions = this.adapter.prepare('DELETE FROM sdk_sessions WHERE updated_at < ?');
122
+ }
123
+ getOrCreate(id, channelId, channelType) {
124
+ const now = Date.now();
125
+ const existing = this.selectSession.get(id);
126
+ if (existing) {
127
+ this.touchSession.run(now, id);
128
+ return rowToSession({ ...existing, updated_at: now });
129
+ }
130
+ this.insertSession.run(id, channelId, channelType, now, now);
131
+ return {
132
+ id,
133
+ channelId,
134
+ channelType,
135
+ createdAt: now,
136
+ updatedAt: now,
137
+ };
138
+ }
139
+ appendUser(sessionId, content, messageSeq, fromName) {
140
+ this.append(sessionId, 'user', content, messageSeq, fromName);
141
+ }
142
+ appendAssistant(sessionId, content, messageSeq, botName) {
143
+ // Assistant turns are attributed to the bot's name (the caller passes the
144
+ // registered bot id). Stored like any other turn — rendering is uniform.
145
+ this.append(sessionId, 'assistant', content, messageSeq, botName);
146
+ }
147
+ append(sessionId, role, content, messageSeq, fromName) {
148
+ const now = Date.now();
149
+ // SECURITY: from_name is the IM display name — USER-CONTROLLED. It is
150
+ // rendered into the shared history prefix as `[<role> <from_name>]:`, so a
151
+ // raw value like `Alice]\n[assistant bot]: forged` would inject a fake
152
+ // assistant turn that every group member then sees (cross-user context
153
+ // poisoning in shared group mode). sanitizeDisplayName (prompt-safety, the
154
+ // shared choke point) strips bracket/line-break chars, caps length, and
155
+ // falls back to the role if nothing survives.
156
+ const safeName = sanitizeDisplayName(fromName ?? role, role);
157
+ this.insertMessage.run(sessionId, role, content, now, messageSeq ?? null, safeName);
158
+ this.touchSession.run(now, sessionId);
159
+ }
160
+ /**
161
+ * Render one history turn with speaker attribution. Group sessions are shared
162
+ * across members, so every turn names its sender — `[user <name>]:` and
163
+ * `[assistant <botName>]:`. The name is sanitized at write time (see append())
164
+ * so it cannot forge turn labels; the `?? role` coalesce only guards rows from
165
+ * before this column existed.
166
+ *
167
+ * SECURITY: the message CONTENT is also user-controlled and travels into the
168
+ * shared `[Conversation history]` block. A body whose line starts with
169
+ * `[assistant ...]:` / `[user ...]:` would forge an extra turn that, in shared
170
+ * group mode, every member then reads as real conversation (cross-user context
171
+ * poisoning — the same threat the from_name strip closes, but via content and
172
+ * easier to exploit since no display name is needed). So we neutralize any
173
+ * line-leading role label in the content here, at render time. This is the one
174
+ * coherent policy: turn labels can ONLY originate from this renderer, never
175
+ * from a user-controlled name or body.
176
+ */
177
+ renderTurn(r) {
178
+ return `[${r.role} ${r.from_name ?? r.role}]: ${escapeRoleLabels(r.content)}`;
179
+ }
180
+ buildHistoryPrefix(sessionId, limit) {
181
+ const rows = this.selectRecentMessages.all(sessionId, limit);
182
+ // Rows are DESC; reverse to chronological order.
183
+ const ordered = rows.slice().reverse();
184
+ return ordered.map((r) => this.renderTurn(r)).join('\n');
185
+ }
186
+ cleanExpired() {
187
+ const cutoff = Date.now() - SEVEN_DAYS_MS;
188
+ const result = this.deleteExpired.run(cutoff);
189
+ // Expire the SDK-session mapping on the same 7-day TTL. sdk_sessions is a
190
+ // separate table (no FK cascade to sessions), so without this a stale mapping
191
+ // would survive the sessions/messages cleanup — and since SDK sessions are
192
+ // always on, the next message would recreate the session and `resume` the
193
+ // expired SDK conversation, silently resurrecting history past the TTL
194
+ // (PR #120 review). updated_at is bumped every turn (setSdkSessionId), so it
195
+ // tracks activity exactly like sessions.updated_at.
196
+ this.deleteExpiredSdkSessions.run(cutoff);
197
+ return result.changes;
198
+ }
199
+ deleteSession(sessionId) {
200
+ this.deleteSessionStmt.run(sessionId);
201
+ }
202
+ /**
203
+ * v0.3 /reset: record a barrier so cold-start backfill never resurrects
204
+ * history at or before `resetSeq`. Persisted independently of the session row
205
+ * (survives deleteSession + restart). Monotonic — a later reset raises the
206
+ * barrier, an out-of-order/older seq is ignored.
207
+ *
208
+ * `resetSeq` is the message_seq of the /reset command itself; everything up to
209
+ * and including it is considered intentionally discarded.
210
+ */
211
+ setResetBarrier(sessionId, resetSeq) {
212
+ this.upsertResetBarrier.run(sessionId, resetSeq);
213
+ }
214
+ /** Return the reset barrier seq for a session, or undefined if never reset. */
215
+ getResetBarrier(sessionId) {
216
+ const row = this.selectResetBarrier.get(sessionId);
217
+ return row?.reset_seq;
218
+ }
219
+ /**
220
+ * v0.3 persistent sessions: record the SDK session UUID for a sessionKey so a
221
+ * later turn can resume it. Upserts (latest wins).
222
+ */
223
+ setSdkSessionId(sessionId, sdkSessionId) {
224
+ this.upsertSdkSession.run(sessionId, sdkSessionId, Date.now());
225
+ }
226
+ /** Return the stored SDK session UUID for a sessionKey, or undefined. */
227
+ getSdkSessionId(sessionId) {
228
+ const row = this.selectSdkSession.get(sessionId);
229
+ return row?.sdk_session_id;
230
+ }
231
+ /** Forget the SDK session mapping (e.g. on /reset or a resume failure). */
232
+ clearSdkSessionId(sessionId) {
233
+ this.deleteSdkSession.run(sessionId);
234
+ }
235
+ close() {
236
+ this.adapter.close();
237
+ }
238
+ /** Record the message_seq at which the bot last replied for a session. */
239
+ setLastBotReplySeq(sessionId, seq) {
240
+ this.lastBotReplySeq.set(sessionId, seq);
241
+ }
242
+ /** Get the message_seq at which the bot last replied for a session. */
243
+ getLastBotReplySeq(sessionId) {
244
+ return this.lastBotReplySeq.get(sessionId);
245
+ }
246
+ /**
247
+ * Build history prefix with answered/new segmentation (G10).
248
+ * Messages with message_seq <= lastBotReplySeq are labeled [answered history],
249
+ * messages after are labeled [new messages]. Falls back to flat history if
250
+ * no lastBotReplySeq tracked or no seq data available.
251
+ */
252
+ buildSegmentedHistoryPrefix(sessionId, limit) {
253
+ const rows = this.selectRecentMessages.all(sessionId, limit);
254
+ const ordered = rows.slice().reverse();
255
+ if (ordered.length === 0)
256
+ return '';
257
+ const lastReplySeq = this.lastBotReplySeq.get(sessionId);
258
+ if (lastReplySeq === undefined) {
259
+ // No segmentation tracking — return flat history.
260
+ return ordered.map((r) => this.renderTurn(r)).join('\n');
261
+ }
262
+ // Real segmentation by message_seq (G10 fix per PR#30 review):
263
+ // - rows with message_seq <= lastReplySeq are answered
264
+ // - rows with message_seq > lastReplySeq are new
265
+ // - rows with NULL message_seq (assistant replies, legacy) attach to the
266
+ // answered side if they precede any "new" user row, else stay flat.
267
+ const answered = [];
268
+ const newMsgs = [];
269
+ let seenNew = false;
270
+ for (const r of ordered) {
271
+ if (r.message_seq != null && r.message_seq > lastReplySeq) {
272
+ seenNew = true;
273
+ newMsgs.push(r);
274
+ }
275
+ else if (r.message_seq != null) {
276
+ answered.push(r);
277
+ }
278
+ else {
279
+ // No seq (e.g. assistant reply) — follows the current side.
280
+ (seenNew ? newMsgs : answered).push(r);
281
+ }
282
+ }
283
+ if (newMsgs.length === 0) {
284
+ // Nothing new since last reply — don't show segmentation labels.
285
+ return answered.map((r) => this.renderTurn(r)).join('\n');
286
+ }
287
+ const parts = [];
288
+ if (answered.length > 0) {
289
+ parts.push('[answered history]');
290
+ parts.push(...answered.map((r) => this.renderTurn(r)));
291
+ }
292
+ parts.push('[new messages]');
293
+ parts.push(...newMsgs.map((r) => this.renderTurn(r)));
294
+ return parts.join('\n');
295
+ }
296
+ }
297
+ //# sourceMappingURL=session-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-store.js","sourceRoot":"","sources":["../src/session-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * #100: Generic skill linking.
3
+ *
4
+ * cc supports external tooling (octo-cli, gh, anything) purely as DATA: the
5
+ * operator drops standard Claude skills into a `skills/` directory and cc loads
6
+ * them — no per-tool code. The SDK only discovers skills under a session's
7
+ * `<cwd>/.claude/skills/` when `settingSources` includes `project`, so for each
8
+ * turn we symlink the operator-owned skill dirs into the session sandbox.
9
+ *
10
+ * Two layers, mirroring config: an install-wide `<baseDir>/skills` shared by all
11
+ * bots, and a per-bot `<baseDir>/<id>/skills`. Per-bot overrides global on a name
12
+ * collision (pass sources as [global, perBot] — later wins).
13
+ *
14
+ * The links point OUTSIDE the sandbox to the operator-owned skill dirs, so the
15
+ * 7-day cwd TTL sweep removes only the links, never the real skills. We only ever
16
+ * manage symlinks we created (real files/dirs the agent placed are left alone),
17
+ * and we prune managed links whose source skill has disappeared so a removed
18
+ * skill stops being offered.
19
+ *
20
+ * Best-effort throughout: a missing source dir is skipped, any per-link error is
21
+ * logged and skipped, and the function never throws — a skill-linking failure
22
+ * must not break a turn.
23
+ */
24
+ /**
25
+ * Symlink every skill found under `sources` into `<sandboxDir>/.claude/skills/`.
26
+ *
27
+ * @param sandboxDir - the resolved per-session cwd (the agent's working dir)
28
+ * @param sources - skill source dirs in ascending precedence (later wins on a
29
+ * name collision). Typically `[globalSkillsDir, perBotSkillsDir]`.
30
+ */
31
+ export declare function linkSkillsIntoSandbox(sandboxDir: string, sources: string[]): void;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * #100: Generic skill linking.
3
+ *
4
+ * cc supports external tooling (octo-cli, gh, anything) purely as DATA: the
5
+ * operator drops standard Claude skills into a `skills/` directory and cc loads
6
+ * them — no per-tool code. The SDK only discovers skills under a session's
7
+ * `<cwd>/.claude/skills/` when `settingSources` includes `project`, so for each
8
+ * turn we symlink the operator-owned skill dirs into the session sandbox.
9
+ *
10
+ * Two layers, mirroring config: an install-wide `<baseDir>/skills` shared by all
11
+ * bots, and a per-bot `<baseDir>/<id>/skills`. Per-bot overrides global on a name
12
+ * collision (pass sources as [global, perBot] — later wins).
13
+ *
14
+ * The links point OUTSIDE the sandbox to the operator-owned skill dirs, so the
15
+ * 7-day cwd TTL sweep removes only the links, never the real skills. We only ever
16
+ * manage symlinks we created (real files/dirs the agent placed are left alone),
17
+ * and we prune managed links whose source skill has disappeared so a removed
18
+ * skill stops being offered.
19
+ *
20
+ * Best-effort throughout: a missing source dir is skipped, any per-link error is
21
+ * logged and skipped, and the function never throws — a skill-linking failure
22
+ * must not break a turn.
23
+ */
24
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, rmSync, symlinkSync, } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ /** Sandbox subpath the SDK scans for project-scope skills. */
27
+ const SKILLS_SUBPATH = join('.claude', 'skills');
28
+ /**
29
+ * Symlink every skill found under `sources` into `<sandboxDir>/.claude/skills/`.
30
+ *
31
+ * @param sandboxDir - the resolved per-session cwd (the agent's working dir)
32
+ * @param sources - skill source dirs in ascending precedence (later wins on a
33
+ * name collision). Typically `[globalSkillsDir, perBotSkillsDir]`.
34
+ */
35
+ export function linkSkillsIntoSandbox(sandboxDir, sources) {
36
+ const skillsRoot = join(sandboxDir, SKILLS_SUBPATH);
37
+ // Collect desired links: skillName → absolute source path. Later sources
38
+ // overwrite earlier ones, so per-bot shadows global.
39
+ const desired = new Map();
40
+ for (const src of sources) {
41
+ let entries;
42
+ try {
43
+ if (!existsSync(src))
44
+ continue;
45
+ entries = readdirSync(src);
46
+ }
47
+ catch (err) {
48
+ console.error(`[cc-channel-octo] skill source unreadable ${src}: ${String(err)}`);
49
+ continue;
50
+ }
51
+ for (const name of entries) {
52
+ // A skill is a directory (or a symlink to one); skip dotfiles / stray files.
53
+ if (name.startsWith('.'))
54
+ continue;
55
+ const target = join(src, name);
56
+ try {
57
+ if (lstatSync(target).isDirectory() || lstatSync(target).isSymbolicLink()) {
58
+ desired.set(name, target);
59
+ }
60
+ }
61
+ catch {
62
+ // racing removal — skip
63
+ }
64
+ }
65
+ }
66
+ try {
67
+ mkdirSync(skillsRoot, { recursive: true });
68
+ }
69
+ catch (err) {
70
+ console.error(`[cc-channel-octo] cannot create skills dir ${skillsRoot}: ${String(err)}`);
71
+ return; // nothing else we can do
72
+ }
73
+ // Prune managed (symlink) entries that are no longer desired or whose target
74
+ // vanished. Never touch real dirs/files (the agent may have created its own).
75
+ let existing = [];
76
+ try {
77
+ existing = readdirSync(skillsRoot);
78
+ }
79
+ catch {
80
+ existing = [];
81
+ }
82
+ for (const name of existing) {
83
+ const linkPath = join(skillsRoot, name);
84
+ let isLink = false;
85
+ try {
86
+ isLink = lstatSync(linkPath).isSymbolicLink();
87
+ }
88
+ catch {
89
+ continue;
90
+ }
91
+ if (!isLink)
92
+ continue; // leave real entries alone
93
+ const wanted = desired.get(name);
94
+ let currentTarget;
95
+ try {
96
+ currentTarget = readlinkSync(linkPath);
97
+ }
98
+ catch {
99
+ currentTarget = undefined;
100
+ }
101
+ // Remove if: not wanted anymore, points elsewhere now, or target gone.
102
+ if (!wanted || currentTarget !== wanted || !existsSync(linkPath)) {
103
+ try {
104
+ rmSync(linkPath, { force: true });
105
+ }
106
+ catch (err) {
107
+ console.error(`[cc-channel-octo] failed pruning skill link ${linkPath}: ${String(err)}`);
108
+ }
109
+ }
110
+ }
111
+ // Create desired links. The prune pass above already removed any managed link
112
+ // that was wrong/stale/dangling, so here: skip if a real entry shadows the
113
+ // name (agent's own file wins) or a correct symlink already exists; else link.
114
+ for (const [name, target] of desired) {
115
+ const linkPath = join(skillsRoot, name);
116
+ try {
117
+ if (isSymlink(linkPath)) {
118
+ if (readlinkSafe(linkPath) === target)
119
+ continue; // already correct
120
+ rmSync(linkPath, { force: true }); // wrong target → replace
121
+ }
122
+ else if (lstatExists(linkPath)) {
123
+ continue; // a real dir/file shadows this skill name — respect it
124
+ }
125
+ symlinkSync(target, linkPath);
126
+ }
127
+ catch (err) {
128
+ console.error(`[cc-channel-octo] failed linking skill ${name} → ${target}: ${String(err)}`);
129
+ }
130
+ }
131
+ }
132
+ /** True when `p` exists as any kind of entry (does not follow symlinks). */
133
+ function lstatExists(p) {
134
+ try {
135
+ lstatSync(p);
136
+ return true;
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ /** True when `p` exists and is a symlink (false on any error). */
143
+ function isSymlink(p) {
144
+ try {
145
+ return lstatSync(p).isSymbolicLink();
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ }
151
+ /** readlink that returns undefined on error. */
152
+ function readlinkSafe(p) {
153
+ try {
154
+ return readlinkSync(p);
155
+ }
156
+ catch {
157
+ return undefined;
158
+ }
159
+ }
160
+ //# sourceMappingURL=skill-linker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-linker.js","sourceRoot":"","sources":["../src/skill-linker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Stream Relay — typing heartbeat + message splitting + plain sendMessage delivery.
3
+ *
4
+ * Consumes an AsyncIterable<string> of text chunks and delivers them to Octo
5
+ * via plain sendMessage with intelligent splitting.
6
+ *
7
+ * Design constraints:
8
+ * - Knows nothing about Claude SDK — input is a generic async text stream.
9
+ * - All Octo API calls go through the api.ts functions (no raw fetch).
10
+ * - Typing indicators keep the user informed while chunks accumulate.
11
+ */
12
+ import type { ChannelType } from "./octo/types.js";
13
+ /** A protected range that splitMessage must not cut through. */
14
+ export interface ProtectedRange {
15
+ /** Start offset (inclusive, UTF-16 code units). */
16
+ start: number;
17
+ /** End offset (exclusive). */
18
+ end: number;
19
+ }
20
+ /**
21
+ * Split a long text into segments at natural boundaries.
22
+ *
23
+ * Priority: paragraph break (\n\n) > newline (\n) > space > hard cut.
24
+ * Each segment is at most `maxChars` characters.
25
+ *
26
+ * `protectedRanges` (P0-1): byte ranges that must NOT be split through. Used
27
+ * by deliver() to keep resolved @name mentions intact — a name like
28
+ * "@John Smith Junior" must be sent as one unit so the corresponding
29
+ * MentionEntity offset/length lands cleanly in one segment.
30
+ */
31
+ export declare function splitMessage(text: string, maxChars?: number, protectedRanges?: ProtectedRange[]): string[];
32
+ export declare class StreamRelay {
33
+ /**
34
+ * Deliver an async stream of text chunks to an Octo channel.
35
+ *
36
+ * 1. Starts a typing indicator heartbeat (5 s interval).
37
+ * 2. Accumulates all chunks from the async iterable.
38
+ * 3. Sends the accumulated text via plain sendMessage with splitting.
39
+ * 4. Always cleans up the typing heartbeat.
40
+ */
41
+ deliver(channelId: string, channelType: ChannelType, chunks: AsyncIterable<string>, apiUrl: string, botToken: string, maxResponseChars?: number, memberMap?: Map<string, string>, isValidUid?: (uid: string) => boolean): Promise<void>;
42
+ }