chainlesschain 0.45.9 → 0.45.11

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 (55) hide show
  1. package/bin/chainlesschain.js +0 -0
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/assets/AppLayout-19ZC8w11.js +1 -0
  4. package/src/assets/web-panel/assets/AppLayout-CjgO-ML6.css +1 -0
  5. package/src/assets/web-panel/assets/Chat-5f__rMCR.js +1 -0
  6. package/src/assets/web-panel/assets/Chat-DB46afPg.css +1 -0
  7. package/src/assets/web-panel/assets/Cron-C4mrNC4c.js +2 -0
  8. package/src/assets/web-panel/assets/Dashboard-CRFnDUFh.css +1 -0
  9. package/src/assets/web-panel/assets/Dashboard-DsjXpZor.js +3 -0
  10. package/src/assets/web-panel/assets/Logs-CC_Zuh66.js +2 -0
  11. package/src/assets/web-panel/assets/Logs-Gf_Mv9Nx.css +1 -0
  12. package/src/assets/web-panel/assets/McpTools-B15GiN3u.js +4 -0
  13. package/src/assets/web-panel/assets/McpTools-CyhSLDwf.css +1 -0
  14. package/src/assets/web-panel/assets/Memory-DRghrGJr.css +1 -0
  15. package/src/assets/web-panel/assets/Memory-Dbd7oLOH.js +2 -0
  16. package/src/assets/web-panel/assets/Notes-BG69sJKi.css +1 -0
  17. package/src/assets/web-panel/assets/Notes-CEkc49fY.js +2 -0
  18. package/src/assets/web-panel/assets/Providers-Brm-S_hS.css +1 -0
  19. package/src/assets/web-panel/assets/Providers-CjyPHW00.js +1 -0
  20. package/src/assets/web-panel/assets/Services-C8Qs6KXv.css +1 -0
  21. package/src/assets/web-panel/assets/Services-XFzHMRRd.js +2 -0
  22. package/src/assets/web-panel/assets/Skills-BdjRyorN.css +1 -0
  23. package/src/assets/web-panel/assets/Skills-D8oxmB3U.js +1 -0
  24. package/src/assets/web-panel/assets/{antd-BhDHu4KM.js → antd-ChLPLhSn.js} +89 -89
  25. package/src/assets/web-panel/assets/chat-C_hu-qNs.js +1 -0
  26. package/src/assets/web-panel/assets/index-CyGyEIVX.css +1 -0
  27. package/src/assets/web-panel/assets/index-DQ5xXK7O.js +2 -0
  28. package/src/assets/web-panel/assets/{markdown-CNQE8HeM.js → markdown-DtbPhnFe.js} +1 -1
  29. package/src/assets/web-panel/assets/parsers-DftYMnlk.js +3 -0
  30. package/src/assets/web-panel/assets/vendor-CN0Iv_qZ.js +1 -0
  31. package/src/assets/web-panel/assets/ws-DwluTqT5.js +1 -0
  32. package/src/assets/web-panel/index.html +4 -4
  33. package/src/commands/config.js +44 -0
  34. package/src/constants.js +1 -0
  35. package/src/lib/background-task-manager.js +305 -0
  36. package/src/lib/background-task-worker.js +50 -0
  37. package/src/lib/feature-flags.js +182 -0
  38. package/src/lib/jsonl-session-store.js +237 -0
  39. package/src/lib/prompt-compressor.js +351 -0
  40. package/src/lib/worktree-isolator.js +231 -0
  41. package/src/repl/agent-repl.js +38 -2
  42. package/src/assets/web-panel/assets/AppLayout-CZDQcPAA.css +0 -1
  43. package/src/assets/web-panel/assets/AppLayout-D-KJRC1s.js +0 -1
  44. package/src/assets/web-panel/assets/Chat-BsgVc34I.js +0 -1
  45. package/src/assets/web-panel/assets/Chat-V-g0UHcr.css +0 -1
  46. package/src/assets/web-panel/assets/Dashboard-Bmgu7fEn.js +0 -2
  47. package/src/assets/web-panel/assets/Providers-DlC9S5Nf.js +0 -2
  48. package/src/assets/web-panel/assets/Providers-YRgg8NIn.css +0 -1
  49. package/src/assets/web-panel/assets/Skills--YIq8avF.js +0 -2
  50. package/src/assets/web-panel/assets/Skills-BC4Yu_4f.css +0 -1
  51. package/src/assets/web-panel/assets/chat-_a36nOu_.js +0 -1
  52. package/src/assets/web-panel/assets/index-B6snAd4S.css +0 -1
  53. package/src/assets/web-panel/assets/index-Ze9tR-zc.js +0 -2
  54. package/src/assets/web-panel/assets/vendor-Ks18OpWK.js +0 -1
  55. package/src/assets/web-panel/assets/ws-Bve9vKht.js +0 -1
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Background Task Manager — daemon task queue with completion notifications.
3
+ *
4
+ * Tasks run in child_process.fork() for isolation.
5
+ * Queue persisted to .chainlesschain/tasks/queue.jsonl.
6
+ * Completion notifications delivered to REPL callback.
7
+ *
8
+ * Feature-flag gated: BACKGROUND_TASKS
9
+ */
10
+
11
+ import { fork } from "node:child_process";
12
+ import {
13
+ existsSync,
14
+ mkdirSync,
15
+ appendFileSync,
16
+ readFileSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { createHash } from "node:crypto";
21
+ import { EventEmitter } from "node:events";
22
+ import { getHomeDir } from "./paths.js";
23
+
24
+ function getTasksDir() {
25
+ const dir = join(getHomeDir(), "tasks");
26
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
27
+ return dir;
28
+ }
29
+
30
+ function queuePath() {
31
+ return join(getTasksDir(), "queue.jsonl");
32
+ }
33
+
34
+ // ── Task Status ─────────────────────────────────────────────────────────
35
+
36
+ export const TaskStatus = {
37
+ PENDING: "pending",
38
+ RUNNING: "running",
39
+ COMPLETED: "completed",
40
+ FAILED: "failed",
41
+ TIMEOUT: "timeout",
42
+ };
43
+
44
+ // ── BackgroundTaskManager ───────────────────────────────────────────────
45
+
46
+ export class BackgroundTaskManager extends EventEmitter {
47
+ /**
48
+ * @param {object} options
49
+ * @param {number} [options.maxConcurrent=3] — Max parallel background tasks
50
+ * @param {number} [options.heartbeatTimeout=60000] — Task heartbeat timeout (ms)
51
+ */
52
+ constructor(options = {}) {
53
+ super();
54
+ this.maxConcurrent = options.maxConcurrent || 3;
55
+ this.heartbeatTimeout = options.heartbeatTimeout || 60000;
56
+ this.tasks = new Map(); // id -> task object
57
+ this.processes = new Map(); // id -> child process
58
+ this._checkInterval = null;
59
+ }
60
+
61
+ /**
62
+ * Create and queue a new background task.
63
+ * @param {object} spec
64
+ * @param {string} spec.type — Task type (e.g. "shell", "agent", "script")
65
+ * @param {string} spec.command — Command or script to execute
66
+ * @param {string} [spec.cwd] — Working directory
67
+ * @param {string} [spec.description] — Human-readable description
68
+ * @returns {object} Created task
69
+ */
70
+ create(spec) {
71
+ if (this._runningCount() >= this.maxConcurrent) {
72
+ throw new Error(
73
+ `Max concurrent tasks reached (${this.maxConcurrent}). Wait for a task to finish.`,
74
+ );
75
+ }
76
+
77
+ const id = `task-${Date.now()}-${createHash("sha256").update(Math.random().toString()).digest("hex").slice(0, 6)}`;
78
+
79
+ const task = {
80
+ id,
81
+ type: spec.type || "shell",
82
+ command: spec.command,
83
+ cwd: spec.cwd || process.cwd(),
84
+ description: spec.description || spec.command,
85
+ status: TaskStatus.PENDING,
86
+ createdAt: Date.now(),
87
+ startedAt: null,
88
+ completedAt: null,
89
+ lastHeartbeat: null,
90
+ result: null,
91
+ error: null,
92
+ };
93
+
94
+ this.tasks.set(id, task);
95
+ this._persistTask(task);
96
+ return task;
97
+ }
98
+
99
+ /**
100
+ * Start a pending task (runs in child process).
101
+ */
102
+ start(taskId) {
103
+ const task = this.tasks.get(taskId);
104
+ if (!task) throw new Error(`Task not found: ${taskId}`);
105
+ if (task.status !== TaskStatus.PENDING)
106
+ throw new Error(`Task ${taskId} is not pending (status: ${task.status})`);
107
+
108
+ task.status = TaskStatus.RUNNING;
109
+ task.startedAt = Date.now();
110
+ task.lastHeartbeat = Date.now();
111
+
112
+ // Create a wrapper script that executes the command
113
+ const child = fork(
114
+ join(import.meta.dirname || ".", "background-task-worker.js"),
115
+ [task.command, task.cwd, task.type],
116
+ {
117
+ cwd: task.cwd,
118
+ silent: true,
119
+ env: { ...process.env, CC_TASK_ID: taskId },
120
+ },
121
+ );
122
+
123
+ this.processes.set(taskId, child);
124
+
125
+ child.on("message", (msg) => {
126
+ if (msg.type === "heartbeat") {
127
+ task.lastHeartbeat = Date.now();
128
+ } else if (msg.type === "result") {
129
+ this._complete(taskId, TaskStatus.COMPLETED, msg.data, null);
130
+ } else if (msg.type === "error") {
131
+ this._complete(taskId, TaskStatus.FAILED, null, msg.error);
132
+ }
133
+ });
134
+
135
+ child.on("exit", (code) => {
136
+ if (task.status === TaskStatus.RUNNING) {
137
+ if (code === 0) {
138
+ this._complete(
139
+ taskId,
140
+ TaskStatus.COMPLETED,
141
+ "Process exited (0)",
142
+ null,
143
+ );
144
+ } else {
145
+ this._complete(
146
+ taskId,
147
+ TaskStatus.FAILED,
148
+ null,
149
+ `Process exited with code ${code}`,
150
+ );
151
+ }
152
+ }
153
+ this.processes.delete(taskId);
154
+ });
155
+
156
+ child.on("error", (err) => {
157
+ this._complete(taskId, TaskStatus.FAILED, null, err.message);
158
+ this.processes.delete(taskId);
159
+ });
160
+
161
+ this._persistTask(task);
162
+ this._ensureHeartbeatChecker();
163
+ return task;
164
+ }
165
+
166
+ /**
167
+ * Create and immediately start a task.
168
+ */
169
+ run(spec) {
170
+ const task = this.create(spec);
171
+ this.start(task.id);
172
+ return task;
173
+ }
174
+
175
+ /**
176
+ * Get a task by ID.
177
+ */
178
+ get(taskId) {
179
+ return this.tasks.get(taskId) || null;
180
+ }
181
+
182
+ /**
183
+ * List all tasks.
184
+ */
185
+ list(filter = {}) {
186
+ let tasks = [...this.tasks.values()];
187
+ if (filter.status) {
188
+ tasks = tasks.filter((t) => t.status === filter.status);
189
+ }
190
+ return tasks.sort((a, b) => b.createdAt - a.createdAt);
191
+ }
192
+
193
+ /**
194
+ * Stop a running task.
195
+ */
196
+ stop(taskId) {
197
+ const child = this.processes.get(taskId);
198
+ if (child) {
199
+ child.kill("SIGTERM");
200
+ setTimeout(() => {
201
+ if (child.exitCode === null) child.kill("SIGKILL");
202
+ }, 2000);
203
+ }
204
+ this._complete(taskId, TaskStatus.FAILED, null, "Stopped by user");
205
+ }
206
+
207
+ /**
208
+ * Clean up completed/failed tasks older than maxAge.
209
+ * @param {number} [maxAge=3600000] — Max age in ms (default 1 hour)
210
+ */
211
+ cleanup(maxAge = 3600000) {
212
+ const cutoff = Date.now() - maxAge;
213
+ let removed = 0;
214
+ for (const [id, task] of this.tasks) {
215
+ if (
216
+ (task.status === TaskStatus.COMPLETED ||
217
+ task.status === TaskStatus.FAILED ||
218
+ task.status === TaskStatus.TIMEOUT) &&
219
+ task.completedAt &&
220
+ task.completedAt < cutoff
221
+ ) {
222
+ this.tasks.delete(id);
223
+ removed++;
224
+ }
225
+ }
226
+ return removed;
227
+ }
228
+
229
+ /**
230
+ * Destroy the manager (kill all running tasks, clear intervals).
231
+ */
232
+ destroy() {
233
+ if (this._checkInterval) {
234
+ clearInterval(this._checkInterval);
235
+ this._checkInterval = null;
236
+ }
237
+ for (const [id] of this.processes) {
238
+ this.stop(id);
239
+ }
240
+ this.tasks.clear();
241
+ this.processes.clear();
242
+ }
243
+
244
+ // ── Internal ──────────────────────────────────────────────────────
245
+
246
+ _complete(taskId, status, result, error) {
247
+ const task = this.tasks.get(taskId);
248
+ if (!task) return;
249
+
250
+ task.status = status;
251
+ task.completedAt = Date.now();
252
+ task.result = result;
253
+ task.error = error;
254
+
255
+ this._persistTask(task);
256
+ this.emit("task:complete", task);
257
+ }
258
+
259
+ _runningCount() {
260
+ let count = 0;
261
+ for (const task of this.tasks.values()) {
262
+ if (task.status === TaskStatus.RUNNING) count++;
263
+ }
264
+ return count;
265
+ }
266
+
267
+ _persistTask(task) {
268
+ try {
269
+ const line = JSON.stringify(task) + "\n";
270
+ appendFileSync(queuePath(), line, "utf-8");
271
+ } catch (_e) {
272
+ // Non-critical
273
+ }
274
+ }
275
+
276
+ _ensureHeartbeatChecker() {
277
+ if (this._checkInterval) return;
278
+
279
+ this._checkInterval = setInterval(
280
+ () => {
281
+ const now = Date.now();
282
+ for (const [id, task] of this.tasks) {
283
+ if (
284
+ task.status === TaskStatus.RUNNING &&
285
+ task.lastHeartbeat &&
286
+ now - task.lastHeartbeat > this.heartbeatTimeout
287
+ ) {
288
+ this._complete(id, TaskStatus.TIMEOUT, null, "Heartbeat timeout");
289
+ const child = this.processes.get(id);
290
+ if (child) {
291
+ child.kill("SIGKILL");
292
+ this.processes.delete(id);
293
+ }
294
+ }
295
+ }
296
+ },
297
+ Math.min(this.heartbeatTimeout / 2, 10000),
298
+ );
299
+
300
+ // Don't keep process alive for the checker
301
+ if (this._checkInterval.unref) {
302
+ this._checkInterval.unref();
303
+ }
304
+ }
305
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Background Task Worker — child process that executes a command.
3
+ *
4
+ * Args: [command, cwd, type]
5
+ * Sends messages to parent: { type: "heartbeat"|"result"|"error", ... }
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+
10
+ const [command, cwd, type] = process.argv.slice(2);
11
+
12
+ // Heartbeat every 5 seconds
13
+ const heartbeat = setInterval(() => {
14
+ if (process.send) process.send({ type: "heartbeat" });
15
+ }, 5000);
16
+
17
+ try {
18
+ let result;
19
+
20
+ if (type === "shell") {
21
+ result = execSync(command, {
22
+ cwd: cwd || process.cwd(),
23
+ encoding: "utf-8",
24
+ timeout: 300000, // 5 min max
25
+ maxBuffer: 10 * 1024 * 1024,
26
+ });
27
+ } else {
28
+ // Default: treat as shell
29
+ result = execSync(command, {
30
+ cwd: cwd || process.cwd(),
31
+ encoding: "utf-8",
32
+ timeout: 300000,
33
+ maxBuffer: 10 * 1024 * 1024,
34
+ });
35
+ }
36
+
37
+ if (process.send) {
38
+ process.send({ type: "result", data: result || "Done" });
39
+ }
40
+ } catch (err) {
41
+ if (process.send) {
42
+ process.send({
43
+ type: "error",
44
+ error: err.stderr || err.message || String(err),
45
+ });
46
+ }
47
+ process.exitCode = 1;
48
+ } finally {
49
+ clearInterval(heartbeat);
50
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Feature Flag System — runtime feature gating for gradual rollout.
3
+ *
4
+ * Flags are stored in .chainlesschain/config.json under "features" key.
5
+ * Each flag can be:
6
+ * - boolean (true/false)
7
+ * - number 0-100 (percentage rollout, hashed by machine-id)
8
+ * - object { enabled, variant, description }
9
+ *
10
+ * Usage:
11
+ * import { feature, featureVariant, listFeatures } from "./feature-flags.js";
12
+ * if (feature("CONTEXT_SNIP")) { ... }
13
+ */
14
+
15
+ import { loadConfig, getConfigValue, saveConfig } from "./config-manager.js";
16
+ import { createHash } from "node:crypto";
17
+ import { hostname } from "node:os";
18
+
19
+ // ── Flag Registry (source of truth for known flags) ────────────────────
20
+
21
+ const FLAG_REGISTRY = {
22
+ BACKGROUND_TASKS: {
23
+ description: "Enable background task queue with daemon execution",
24
+ default: false,
25
+ },
26
+ WORKTREE_ISOLATION: {
27
+ description: "Enable git worktree isolation for agent tasks",
28
+ default: false,
29
+ },
30
+ CONTEXT_SNIP: {
31
+ description: "Enable snipCompact strategy in context compression",
32
+ default: false,
33
+ },
34
+ CONTEXT_COLLAPSE: {
35
+ description: "Enable contextCollapse strategy in context compression",
36
+ default: false,
37
+ },
38
+ JSONL_SESSION: {
39
+ description: "Use JSONL append-only format for session persistence",
40
+ default: false,
41
+ },
42
+ PROMPT_COMPRESSOR: {
43
+ description: "Enable CLI prompt compressor (auto/snip/collapse)",
44
+ default: true,
45
+ },
46
+ };
47
+
48
+ // ── Core API ────────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Check if a feature flag is enabled.
52
+ * @param {string} name - Flag name (e.g. "CONTEXT_SNIP")
53
+ * @returns {boolean}
54
+ */
55
+ export function feature(name) {
56
+ const value = _resolve(name);
57
+ if (typeof value === "boolean") return value;
58
+ if (typeof value === "number") return _percentageCheck(name, value);
59
+ if (value && typeof value === "object") return Boolean(value.enabled);
60
+ return _getDefault(name);
61
+ }
62
+
63
+ /**
64
+ * Get the variant string for a feature (for A/B style flags).
65
+ * @param {string} name
66
+ * @returns {string|null}
67
+ */
68
+ export function featureVariant(name) {
69
+ const value = _resolve(name);
70
+ if (value && typeof value === "object" && value.variant) {
71
+ return value.variant;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * List all known feature flags with their current values.
78
+ * @returns {Array<{name, enabled, description, source, raw}>}
79
+ */
80
+ export function listFeatures() {
81
+ const config = loadConfig();
82
+ const features = config.features || {};
83
+ const result = [];
84
+
85
+ for (const [name, meta] of Object.entries(FLAG_REGISTRY)) {
86
+ const raw = features[name];
87
+ const enabled = feature(name);
88
+ const source =
89
+ raw !== undefined
90
+ ? "config"
91
+ : process.env[`CC_FLAG_${name}`] !== undefined
92
+ ? "env"
93
+ : "default";
94
+
95
+ result.push({
96
+ name,
97
+ enabled,
98
+ description: meta.description,
99
+ source,
100
+ raw: raw !== undefined ? raw : meta.default,
101
+ });
102
+ }
103
+
104
+ // Include unknown flags from config (user-defined)
105
+ for (const [name, raw] of Object.entries(features)) {
106
+ if (!FLAG_REGISTRY[name]) {
107
+ result.push({
108
+ name,
109
+ enabled: Boolean(raw),
110
+ description: "(user-defined)",
111
+ source: "config",
112
+ raw,
113
+ });
114
+ }
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Set a feature flag value in config.
122
+ * @param {string} name
123
+ * @param {boolean|number|string} value
124
+ */
125
+ export function setFeature(name, value) {
126
+ const config = loadConfig();
127
+ if (!config.features) config.features = {};
128
+ config.features[name] = value;
129
+ saveConfig(config);
130
+ }
131
+
132
+ /**
133
+ * Get the registry entry for a known flag (or null).
134
+ * @param {string} name
135
+ * @returns {{description: string, default: boolean}|null}
136
+ */
137
+ export function getFlagInfo(name) {
138
+ return FLAG_REGISTRY[name] || null;
139
+ }
140
+
141
+ // ── Internal helpers ───────────────────────────────���────────────────────
142
+
143
+ function _resolve(name) {
144
+ // Priority: env var > config > default
145
+ const envKey = `CC_FLAG_${name}`;
146
+ if (process.env[envKey] !== undefined) {
147
+ const envVal = process.env[envKey];
148
+ if (envVal === "true" || envVal === "1") return true;
149
+ if (envVal === "false" || envVal === "0") return false;
150
+ const num = Number(envVal);
151
+ if (!isNaN(num)) return num;
152
+ return envVal;
153
+ }
154
+
155
+ const configVal = getConfigValue(`features.${name}`);
156
+ if (configVal !== undefined) return configVal;
157
+
158
+ return undefined; // let caller fall to default
159
+ }
160
+
161
+ function _getDefault(name) {
162
+ const meta = FLAG_REGISTRY[name];
163
+ return meta ? meta.default : false;
164
+ }
165
+
166
+ function _percentageCheck(name, percentage) {
167
+ if (percentage <= 0) return false;
168
+ if (percentage >= 100) return true;
169
+ const hash = createHash("md5")
170
+ .update(`${name}:${_machineId()}`)
171
+ .digest("hex");
172
+ const bucket = parseInt(hash.slice(0, 8), 16) % 100;
173
+ return bucket < percentage;
174
+ }
175
+
176
+ let _cachedMachineId = null;
177
+ function _machineId() {
178
+ if (!_cachedMachineId) {
179
+ _cachedMachineId = hostname() || "unknown";
180
+ }
181
+ return _cachedMachineId;
182
+ }