nextclaw 0.3.3 → 0.4.1
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/dist/cli/index.js +513 -191
- package/package.json +3 -3
- package/templates/AGENTS.md +66 -7
- package/templates/BOOT.md +4 -0
- package/templates/BOOTSTRAP.md +18 -0
- package/templates/HEARTBEAT.md +5 -0
- package/templates/IDENTITY.md +11 -0
- package/templates/MEMORY.md +15 -0
- package/templates/SOUL.md +23 -10
- package/templates/TOOLS.md +20 -0
- package/templates/USER.md +12 -6
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
saveConfig,
|
|
11
11
|
getConfigPath,
|
|
12
12
|
getDataDir as getDataDir2,
|
|
13
|
-
ConfigSchema,
|
|
13
|
+
ConfigSchema as ConfigSchema2,
|
|
14
14
|
getApiBase,
|
|
15
15
|
getProvider,
|
|
16
16
|
getProviderName,
|
|
@@ -35,19 +35,29 @@ import { startUiServer } from "nextclaw-server";
|
|
|
35
35
|
import {
|
|
36
36
|
closeSync,
|
|
37
37
|
cpSync,
|
|
38
|
-
existsSync as
|
|
38
|
+
existsSync as existsSync3,
|
|
39
39
|
mkdirSync as mkdirSync2,
|
|
40
40
|
openSync,
|
|
41
|
-
readFileSync as
|
|
41
|
+
readFileSync as readFileSync3,
|
|
42
42
|
rmSync as rmSync2,
|
|
43
43
|
writeFileSync as writeFileSync2
|
|
44
44
|
} from "fs";
|
|
45
|
-
import { dirname, join as
|
|
46
|
-
import { spawn as spawn2, spawnSync } from "child_process";
|
|
45
|
+
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
46
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
47
47
|
import { createInterface } from "readline";
|
|
48
|
-
import { fileURLToPath as
|
|
48
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
49
49
|
import chokidar from "chokidar";
|
|
50
50
|
|
|
51
|
+
// src/cli/gateway/controller.ts
|
|
52
|
+
import { createHash } from "crypto";
|
|
53
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
54
|
+
import { spawnSync } from "child_process";
|
|
55
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
56
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
57
|
+
import {
|
|
58
|
+
ConfigSchema
|
|
59
|
+
} from "nextclaw-core";
|
|
60
|
+
|
|
51
61
|
// src/cli/utils.ts
|
|
52
62
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
53
63
|
import { join, resolve } from "path";
|
|
@@ -86,12 +96,12 @@ async function isPortAvailable(port, host) {
|
|
|
86
96
|
return await canBindPort(port, checkHost);
|
|
87
97
|
}
|
|
88
98
|
async function canBindPort(port, host) {
|
|
89
|
-
return await new Promise((
|
|
99
|
+
return await new Promise((resolve4) => {
|
|
90
100
|
const server = createServer();
|
|
91
101
|
server.unref();
|
|
92
|
-
server.once("error", () =>
|
|
102
|
+
server.once("error", () => resolve4(false));
|
|
93
103
|
server.listen({ port, host }, () => {
|
|
94
|
-
server.close(() =>
|
|
104
|
+
server.close(() => resolve4(true));
|
|
95
105
|
});
|
|
96
106
|
});
|
|
97
107
|
}
|
|
@@ -149,7 +159,7 @@ async function waitForExit(pid, timeoutMs) {
|
|
|
149
159
|
if (!isProcessRunning(pid)) {
|
|
150
160
|
return true;
|
|
151
161
|
}
|
|
152
|
-
await new Promise((
|
|
162
|
+
await new Promise((resolve4) => setTimeout(resolve4, 200));
|
|
153
163
|
}
|
|
154
164
|
return !isProcessRunning(pid);
|
|
155
165
|
}
|
|
@@ -280,14 +290,395 @@ function printAgentResponse(response) {
|
|
|
280
290
|
async function prompt(rl, question) {
|
|
281
291
|
rl.setPrompt(question);
|
|
282
292
|
rl.prompt();
|
|
283
|
-
return new Promise((
|
|
284
|
-
rl.once("line", (line) =>
|
|
293
|
+
return new Promise((resolve4) => {
|
|
294
|
+
rl.once("line", (line) => resolve4(line));
|
|
285
295
|
});
|
|
286
296
|
}
|
|
287
297
|
|
|
298
|
+
// src/cli/gateway/controller.ts
|
|
299
|
+
var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
|
|
300
|
+
var redactConfig = (value) => {
|
|
301
|
+
if (Array.isArray(value)) {
|
|
302
|
+
return value.map((entry) => redactConfig(entry));
|
|
303
|
+
}
|
|
304
|
+
if (!value || typeof value !== "object") {
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
const entries = value;
|
|
308
|
+
const output = {};
|
|
309
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
310
|
+
if (/apiKey|token|secret|password|appId|clientSecret|accessKey/i.test(key)) {
|
|
311
|
+
output[key] = val ? "***" : val;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
output[key] = redactConfig(val);
|
|
315
|
+
}
|
|
316
|
+
return output;
|
|
317
|
+
};
|
|
318
|
+
var readConfigSnapshot = (getConfigPath2) => {
|
|
319
|
+
const path = getConfigPath2();
|
|
320
|
+
let raw = "";
|
|
321
|
+
let parsed = {};
|
|
322
|
+
if (existsSync2(path)) {
|
|
323
|
+
raw = readFileSync2(path, "utf-8");
|
|
324
|
+
try {
|
|
325
|
+
parsed = JSON.parse(raw);
|
|
326
|
+
} catch {
|
|
327
|
+
parsed = {};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let config;
|
|
331
|
+
let valid = true;
|
|
332
|
+
try {
|
|
333
|
+
config = ConfigSchema.parse(parsed);
|
|
334
|
+
} catch {
|
|
335
|
+
config = ConfigSchema.parse({});
|
|
336
|
+
valid = false;
|
|
337
|
+
}
|
|
338
|
+
if (!raw) {
|
|
339
|
+
raw = JSON.stringify(config, null, 2);
|
|
340
|
+
}
|
|
341
|
+
const hash = hashRaw(raw);
|
|
342
|
+
const redacted = redactConfig(config);
|
|
343
|
+
return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config, redacted, valid };
|
|
344
|
+
};
|
|
345
|
+
var mergeDeep = (base, patch) => {
|
|
346
|
+
const next = { ...base };
|
|
347
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
348
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
349
|
+
const baseVal = base[key];
|
|
350
|
+
if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
|
|
351
|
+
next[key] = mergeDeep(baseVal, value);
|
|
352
|
+
} else {
|
|
353
|
+
next[key] = mergeDeep({}, value);
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
next[key] = value;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return next;
|
|
360
|
+
};
|
|
361
|
+
var buildSchemaFromValue = (value) => {
|
|
362
|
+
if (Array.isArray(value)) {
|
|
363
|
+
const item = value.length ? buildSchemaFromValue(value[0]) : { type: "string" };
|
|
364
|
+
return { type: "array", items: item };
|
|
365
|
+
}
|
|
366
|
+
if (value && typeof value === "object") {
|
|
367
|
+
const props = {};
|
|
368
|
+
for (const [key, val] of Object.entries(value)) {
|
|
369
|
+
props[key] = buildSchemaFromValue(val);
|
|
370
|
+
}
|
|
371
|
+
return { type: "object", properties: props };
|
|
372
|
+
}
|
|
373
|
+
if (typeof value === "number") {
|
|
374
|
+
return { type: "number" };
|
|
375
|
+
}
|
|
376
|
+
if (typeof value === "boolean") {
|
|
377
|
+
return { type: "boolean" };
|
|
378
|
+
}
|
|
379
|
+
if (value === null) {
|
|
380
|
+
return { type: ["null", "string"] };
|
|
381
|
+
}
|
|
382
|
+
return { type: "string" };
|
|
383
|
+
};
|
|
384
|
+
var scheduleRestart = (delayMs, reason) => {
|
|
385
|
+
const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
|
|
386
|
+
console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}, delay);
|
|
390
|
+
};
|
|
391
|
+
var GatewayControllerImpl = class {
|
|
392
|
+
constructor(deps) {
|
|
393
|
+
this.deps = deps;
|
|
394
|
+
}
|
|
395
|
+
status() {
|
|
396
|
+
return {
|
|
397
|
+
channels: this.deps.reloader.getChannels().enabledChannels,
|
|
398
|
+
cron: this.deps.cron.status(),
|
|
399
|
+
configPath: this.deps.getConfigPath()
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async reloadConfig(reason) {
|
|
403
|
+
return this.deps.reloader.reloadConfig(reason);
|
|
404
|
+
}
|
|
405
|
+
async restart(options) {
|
|
406
|
+
scheduleRestart(options?.delayMs, options?.reason);
|
|
407
|
+
return "Restart scheduled";
|
|
408
|
+
}
|
|
409
|
+
async getConfig() {
|
|
410
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
411
|
+
return {
|
|
412
|
+
raw: snapshot.raw,
|
|
413
|
+
hash: snapshot.hash,
|
|
414
|
+
path: this.deps.getConfigPath(),
|
|
415
|
+
config: snapshot.redacted,
|
|
416
|
+
parsed: snapshot.redacted,
|
|
417
|
+
resolved: snapshot.redacted,
|
|
418
|
+
valid: snapshot.valid
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async getConfigSchema() {
|
|
422
|
+
const base = ConfigSchema.parse({});
|
|
423
|
+
return {
|
|
424
|
+
schema: {
|
|
425
|
+
...buildSchemaFromValue(base),
|
|
426
|
+
title: "NextClawConfig",
|
|
427
|
+
description: "NextClaw config schema (simplified)"
|
|
428
|
+
},
|
|
429
|
+
uiHints: {},
|
|
430
|
+
version: getPackageVersion(),
|
|
431
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async applyConfig(params) {
|
|
435
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
436
|
+
if (!params.baseHash) {
|
|
437
|
+
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
438
|
+
}
|
|
439
|
+
if (!snapshot.valid || !snapshot.hash) {
|
|
440
|
+
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
441
|
+
}
|
|
442
|
+
if (params.baseHash !== snapshot.hash) {
|
|
443
|
+
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
444
|
+
}
|
|
445
|
+
let parsedRaw;
|
|
446
|
+
try {
|
|
447
|
+
parsedRaw = JSON.parse(params.raw);
|
|
448
|
+
} catch {
|
|
449
|
+
return { ok: false, error: "invalid JSON in raw config" };
|
|
450
|
+
}
|
|
451
|
+
let validated;
|
|
452
|
+
try {
|
|
453
|
+
validated = ConfigSchema.parse(parsedRaw);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
456
|
+
}
|
|
457
|
+
this.deps.saveConfig(validated);
|
|
458
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
459
|
+
scheduleRestart(delayMs, "config.apply");
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
note: params.note ?? null,
|
|
463
|
+
path: this.deps.getConfigPath(),
|
|
464
|
+
config: redactConfig(validated),
|
|
465
|
+
restart: { scheduled: true, delayMs }
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
async patchConfig(params) {
|
|
469
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
470
|
+
if (!params.baseHash) {
|
|
471
|
+
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
472
|
+
}
|
|
473
|
+
if (!snapshot.valid || !snapshot.hash) {
|
|
474
|
+
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
475
|
+
}
|
|
476
|
+
if (params.baseHash !== snapshot.hash) {
|
|
477
|
+
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
478
|
+
}
|
|
479
|
+
let patch;
|
|
480
|
+
try {
|
|
481
|
+
patch = JSON.parse(params.raw);
|
|
482
|
+
} catch {
|
|
483
|
+
return { ok: false, error: "invalid JSON in raw config" };
|
|
484
|
+
}
|
|
485
|
+
const merged = mergeDeep(snapshot.config, patch);
|
|
486
|
+
let validated;
|
|
487
|
+
try {
|
|
488
|
+
validated = ConfigSchema.parse(merged);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
491
|
+
}
|
|
492
|
+
this.deps.saveConfig(validated);
|
|
493
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
494
|
+
scheduleRestart(delayMs, "config.patch");
|
|
495
|
+
return {
|
|
496
|
+
ok: true,
|
|
497
|
+
note: params.note ?? null,
|
|
498
|
+
path: this.deps.getConfigPath(),
|
|
499
|
+
config: redactConfig(validated),
|
|
500
|
+
restart: { scheduled: true, delayMs }
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async updateRun(params) {
|
|
504
|
+
const timeoutMs = params.timeoutMs ?? 20 * 6e4;
|
|
505
|
+
const gatewayDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
|
|
506
|
+
const cliDir = resolve2(gatewayDir, "..");
|
|
507
|
+
const pkgRoot = resolve2(cliDir, "..", "..");
|
|
508
|
+
const repoRoot = existsSync2(join2(pkgRoot, ".git")) ? pkgRoot : resolve2(pkgRoot, "..", "..");
|
|
509
|
+
const steps = [];
|
|
510
|
+
const runStep = (cmd, args, cwd) => {
|
|
511
|
+
const result = spawnSync(cmd, args, {
|
|
512
|
+
cwd,
|
|
513
|
+
encoding: "utf-8",
|
|
514
|
+
timeout: timeoutMs,
|
|
515
|
+
stdio: "pipe"
|
|
516
|
+
});
|
|
517
|
+
const step = {
|
|
518
|
+
cmd,
|
|
519
|
+
args,
|
|
520
|
+
cwd,
|
|
521
|
+
code: result.status,
|
|
522
|
+
stdout: (result.stdout ?? "").toString().slice(0, 4e3),
|
|
523
|
+
stderr: (result.stderr ?? "").toString().slice(0, 4e3)
|
|
524
|
+
};
|
|
525
|
+
steps.push(step);
|
|
526
|
+
return { ok: result.status === 0, code: result.status };
|
|
527
|
+
};
|
|
528
|
+
const updateCommand = process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
|
|
529
|
+
if (updateCommand) {
|
|
530
|
+
const ok = runStep("sh", ["-c", updateCommand], process.cwd());
|
|
531
|
+
if (!ok.ok) {
|
|
532
|
+
return { ok: false, error: "update command failed", steps };
|
|
533
|
+
}
|
|
534
|
+
} else if (existsSync2(join2(repoRoot, ".git"))) {
|
|
535
|
+
if (!which("git")) {
|
|
536
|
+
return { ok: false, error: "git not found for repo update", steps };
|
|
537
|
+
}
|
|
538
|
+
const ok = runStep("git", ["-C", repoRoot, "pull", "--rebase"], repoRoot);
|
|
539
|
+
if (!ok.ok) {
|
|
540
|
+
return { ok: false, error: "git pull failed", steps };
|
|
541
|
+
}
|
|
542
|
+
if (existsSync2(join2(repoRoot, "pnpm-lock.yaml")) && which("pnpm")) {
|
|
543
|
+
const installOk = runStep("pnpm", ["install"], repoRoot);
|
|
544
|
+
if (!installOk.ok) {
|
|
545
|
+
return { ok: false, error: "pnpm install failed", steps };
|
|
546
|
+
}
|
|
547
|
+
} else if (existsSync2(join2(repoRoot, "package.json")) && which("npm")) {
|
|
548
|
+
const installOk = runStep("npm", ["install"], repoRoot);
|
|
549
|
+
if (!installOk.ok) {
|
|
550
|
+
return { ok: false, error: "npm install failed", steps };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else if (which("npm")) {
|
|
554
|
+
const ok = runStep("npm", ["i", "-g", "nextclaw"], process.cwd());
|
|
555
|
+
if (!ok.ok) {
|
|
556
|
+
return { ok: false, error: "npm install -g nextclaw failed", steps };
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
return { ok: false, error: "no update strategy available", steps };
|
|
560
|
+
}
|
|
561
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
562
|
+
scheduleRestart(delayMs, "update.run");
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
note: params.note ?? null,
|
|
566
|
+
restart: { scheduled: true, delayMs },
|
|
567
|
+
steps
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
288
572
|
// src/cli/runtime.ts
|
|
289
573
|
var LOGO = "\u{1F916}";
|
|
290
574
|
var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
|
|
575
|
+
var ConfigReloader = class {
|
|
576
|
+
constructor(options) {
|
|
577
|
+
this.options = options;
|
|
578
|
+
this.currentConfig = options.initialConfig;
|
|
579
|
+
this.channels = options.channels;
|
|
580
|
+
}
|
|
581
|
+
currentConfig;
|
|
582
|
+
channels;
|
|
583
|
+
reloadTask = null;
|
|
584
|
+
providerReloadTask = null;
|
|
585
|
+
reloadTimer = null;
|
|
586
|
+
reloadRunning = false;
|
|
587
|
+
reloadPending = false;
|
|
588
|
+
getChannels() {
|
|
589
|
+
return this.channels;
|
|
590
|
+
}
|
|
591
|
+
async applyReloadPlan(nextConfig) {
|
|
592
|
+
const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
|
|
593
|
+
if (!changedPaths.length) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.currentConfig = nextConfig;
|
|
597
|
+
const plan = buildReloadPlan(changedPaths);
|
|
598
|
+
if (plan.restartChannels) {
|
|
599
|
+
await this.reloadChannels(nextConfig);
|
|
600
|
+
}
|
|
601
|
+
if (plan.reloadProviders) {
|
|
602
|
+
await this.reloadProvider(nextConfig);
|
|
603
|
+
}
|
|
604
|
+
if (plan.restartRequired.length > 0) {
|
|
605
|
+
this.options.onRestartRequired(plan.restartRequired);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
scheduleReload(reason) {
|
|
609
|
+
if (this.reloadTimer) {
|
|
610
|
+
clearTimeout(this.reloadTimer);
|
|
611
|
+
}
|
|
612
|
+
this.reloadTimer = setTimeout(() => {
|
|
613
|
+
void this.runReload(reason);
|
|
614
|
+
}, 300);
|
|
615
|
+
}
|
|
616
|
+
async runReload(reason) {
|
|
617
|
+
if (this.reloadRunning) {
|
|
618
|
+
this.reloadPending = true;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.reloadRunning = true;
|
|
622
|
+
if (this.reloadTimer) {
|
|
623
|
+
clearTimeout(this.reloadTimer);
|
|
624
|
+
this.reloadTimer = null;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
const nextConfig = this.options.loadConfig();
|
|
628
|
+
await this.applyReloadPlan(nextConfig);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
631
|
+
} finally {
|
|
632
|
+
this.reloadRunning = false;
|
|
633
|
+
if (this.reloadPending) {
|
|
634
|
+
this.reloadPending = false;
|
|
635
|
+
this.scheduleReload("pending");
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async reloadConfig(reason) {
|
|
640
|
+
await this.runReload(reason ?? "gateway tool");
|
|
641
|
+
return "Config reload triggered";
|
|
642
|
+
}
|
|
643
|
+
async reloadChannels(nextConfig) {
|
|
644
|
+
if (this.reloadTask) {
|
|
645
|
+
await this.reloadTask;
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.reloadTask = (async () => {
|
|
649
|
+
await this.channels.stopAll();
|
|
650
|
+
this.channels = new ChannelManager(nextConfig, this.options.bus, this.options.sessionManager);
|
|
651
|
+
await this.channels.startAll();
|
|
652
|
+
})();
|
|
653
|
+
try {
|
|
654
|
+
await this.reloadTask;
|
|
655
|
+
} finally {
|
|
656
|
+
this.reloadTask = null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async reloadProvider(nextConfig) {
|
|
660
|
+
if (!this.options.providerManager) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (this.providerReloadTask) {
|
|
664
|
+
await this.providerReloadTask;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
this.providerReloadTask = (async () => {
|
|
668
|
+
const nextProvider = this.options.makeProvider(nextConfig);
|
|
669
|
+
if (!nextProvider) {
|
|
670
|
+
console.warn("Provider reload skipped: missing API key.");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
this.options.providerManager?.set(nextProvider);
|
|
674
|
+
})();
|
|
675
|
+
try {
|
|
676
|
+
await this.providerReloadTask;
|
|
677
|
+
} finally {
|
|
678
|
+
this.providerReloadTask = null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
};
|
|
291
682
|
var CliRuntime = class {
|
|
292
683
|
logo;
|
|
293
684
|
constructor(options = {}) {
|
|
@@ -303,19 +694,20 @@ var CliRuntime = class {
|
|
|
303
694
|
async init(options = {}) {
|
|
304
695
|
const source = options.source ?? "init";
|
|
305
696
|
const prefix = options.auto ? "Auto init" : "Init";
|
|
697
|
+
const force = Boolean(options.force);
|
|
306
698
|
const configPath = getConfigPath();
|
|
307
699
|
let createdConfig = false;
|
|
308
|
-
if (!
|
|
309
|
-
const config2 =
|
|
700
|
+
if (!existsSync3(configPath)) {
|
|
701
|
+
const config2 = ConfigSchema2.parse({});
|
|
310
702
|
saveConfig(config2);
|
|
311
703
|
createdConfig = true;
|
|
312
704
|
}
|
|
313
705
|
const config = loadConfig();
|
|
314
706
|
const workspaceSetting = config.agents.defaults.workspace;
|
|
315
|
-
const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ?
|
|
316
|
-
const workspaceExisted =
|
|
707
|
+
const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join3(getDataDir2(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
|
|
708
|
+
const workspaceExisted = existsSync3(workspacePath);
|
|
317
709
|
mkdirSync2(workspacePath, { recursive: true });
|
|
318
|
-
const templateResult = this.createWorkspaceTemplates(workspacePath);
|
|
710
|
+
const templateResult = this.createWorkspaceTemplates(workspacePath, { force });
|
|
319
711
|
if (createdConfig) {
|
|
320
712
|
console.log(`\u2713 ${prefix}: created config at ${configPath}`);
|
|
321
713
|
}
|
|
@@ -335,7 +727,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
335
727
|
console.log(` 1. Add your API key to ${configPath}`);
|
|
336
728
|
console.log(` 2. Chat: ${APP_NAME} agent -m "Hello!"`);
|
|
337
729
|
} else {
|
|
338
|
-
console.log(`Tip: Run "${APP_NAME} init" to re-run initialization if needed.`);
|
|
730
|
+
console.log(`Tip: Run "${APP_NAME} init${force ? " --force" : ""}" to re-run initialization if needed.`);
|
|
339
731
|
}
|
|
340
732
|
}
|
|
341
733
|
async gateway(opts) {
|
|
@@ -462,7 +854,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
462
854
|
workspace: getWorkspacePath(config.agents.defaults.workspace),
|
|
463
855
|
braveApiKey: config.tools.web.search.apiKey || void 0,
|
|
464
856
|
execConfig: config.tools.exec,
|
|
465
|
-
restrictToWorkspace: config.tools.restrictToWorkspace
|
|
857
|
+
restrictToWorkspace: config.tools.restrictToWorkspace,
|
|
858
|
+
contextConfig: config.agents.context
|
|
466
859
|
});
|
|
467
860
|
if (opts.message) {
|
|
468
861
|
const response = await agentLoop.processDirect({
|
|
@@ -476,10 +869,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
476
869
|
}
|
|
477
870
|
console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
|
|
478
871
|
`);
|
|
479
|
-
const historyFile =
|
|
480
|
-
const historyDir =
|
|
872
|
+
const historyFile = join3(getDataDir2(), "history", "cli_history");
|
|
873
|
+
const historyDir = resolve3(historyFile, "..");
|
|
481
874
|
mkdirSync2(historyDir, { recursive: true });
|
|
482
|
-
const history =
|
|
875
|
+
const history = existsSync3(historyFile) ? readFileSync3(historyFile, "utf-8").split("\n").filter(Boolean) : [];
|
|
483
876
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
484
877
|
rl.on("close", () => {
|
|
485
878
|
const merged = history.concat(rl.history ?? []);
|
|
@@ -520,13 +913,13 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
520
913
|
const bridgeDir = this.getBridgeDir();
|
|
521
914
|
console.log(`${this.logo} Starting bridge...`);
|
|
522
915
|
console.log("Scan the QR code to connect.\n");
|
|
523
|
-
const result =
|
|
916
|
+
const result = spawnSync2("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
|
|
524
917
|
if (result.status !== 0) {
|
|
525
918
|
console.error(`Bridge failed: ${result.status ?? 1}`);
|
|
526
919
|
}
|
|
527
920
|
}
|
|
528
921
|
cronList(opts) {
|
|
529
|
-
const storePath =
|
|
922
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
530
923
|
const service = new CronService(storePath);
|
|
531
924
|
const jobs = service.listJobs(Boolean(opts.all));
|
|
532
925
|
if (!jobs.length) {
|
|
@@ -546,7 +939,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
546
939
|
}
|
|
547
940
|
}
|
|
548
941
|
cronAdd(opts) {
|
|
549
|
-
const storePath =
|
|
942
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
550
943
|
const service = new CronService(storePath);
|
|
551
944
|
let schedule = null;
|
|
552
945
|
if (opts.every) {
|
|
@@ -571,7 +964,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
571
964
|
console.log(`\u2713 Added job '${job.name}' (${job.id})`);
|
|
572
965
|
}
|
|
573
966
|
cronRemove(jobId) {
|
|
574
|
-
const storePath =
|
|
967
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
575
968
|
const service = new CronService(storePath);
|
|
576
969
|
if (service.removeJob(jobId)) {
|
|
577
970
|
console.log(`\u2713 Removed job ${jobId}`);
|
|
@@ -580,7 +973,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
580
973
|
}
|
|
581
974
|
}
|
|
582
975
|
cronEnable(jobId, opts) {
|
|
583
|
-
const storePath =
|
|
976
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
584
977
|
const service = new CronService(storePath);
|
|
585
978
|
const job = service.enableJob(jobId, !opts.disable);
|
|
586
979
|
if (job) {
|
|
@@ -590,7 +983,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
590
983
|
}
|
|
591
984
|
}
|
|
592
985
|
async cronRun(jobId, opts) {
|
|
593
|
-
const storePath =
|
|
986
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
594
987
|
const service = new CronService(storePath);
|
|
595
988
|
const ok = await service.runJob(jobId, Boolean(opts.force));
|
|
596
989
|
console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
|
|
@@ -601,8 +994,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
601
994
|
const workspace = getWorkspacePath(config.agents.defaults.workspace);
|
|
602
995
|
console.log(`${this.logo} ${APP_NAME} Status
|
|
603
996
|
`);
|
|
604
|
-
console.log(`Config: ${configPath} ${
|
|
605
|
-
console.log(`Workspace: ${workspace} ${
|
|
997
|
+
console.log(`Config: ${configPath} ${existsSync3(configPath) ? "\u2713" : "\u2717"}`);
|
|
998
|
+
console.log(`Workspace: ${workspace} ${existsSync3(workspace) ? "\u2713" : "\u2717"}`);
|
|
606
999
|
console.log(`Model: ${config.agents.defaults.model}`);
|
|
607
1000
|
for (const spec of PROVIDERS) {
|
|
608
1001
|
const provider = config.providers[spec.name];
|
|
@@ -622,32 +1015,36 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
622
1015
|
const provider = options.allowMissingProvider === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config);
|
|
623
1016
|
const providerManager = provider ? new ProviderManager(provider) : null;
|
|
624
1017
|
const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
|
|
625
|
-
const cronStorePath =
|
|
1018
|
+
const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
626
1019
|
const cron2 = new CronService(cronStorePath);
|
|
627
1020
|
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
628
1021
|
const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
|
|
629
1022
|
if (!provider) {
|
|
630
|
-
|
|
631
|
-
const uiServer = startUiServer({
|
|
632
|
-
host: uiConfig.host,
|
|
633
|
-
port: uiConfig.port,
|
|
634
|
-
configPath: getConfigPath(),
|
|
635
|
-
staticDir: uiStaticDir ?? void 0
|
|
636
|
-
});
|
|
637
|
-
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
638
|
-
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
639
|
-
if (uiStaticDir) {
|
|
640
|
-
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
641
|
-
}
|
|
642
|
-
if (uiConfig.open) {
|
|
643
|
-
openBrowser(uiUrl);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
1023
|
+
this.startUiIfEnabled(uiConfig, uiStaticDir);
|
|
646
1024
|
console.log("Warning: No API key configured. UI server only.");
|
|
647
1025
|
await new Promise(() => {
|
|
648
1026
|
});
|
|
649
1027
|
return;
|
|
650
1028
|
}
|
|
1029
|
+
const channels2 = new ChannelManager(config, bus, sessionManager);
|
|
1030
|
+
const reloader = new ConfigReloader({
|
|
1031
|
+
initialConfig: config,
|
|
1032
|
+
channels: channels2,
|
|
1033
|
+
bus,
|
|
1034
|
+
sessionManager,
|
|
1035
|
+
providerManager,
|
|
1036
|
+
makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }),
|
|
1037
|
+
loadConfig,
|
|
1038
|
+
onRestartRequired: (paths) => {
|
|
1039
|
+
console.warn(`Config changes require restart: ${paths.join(", ")}`);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
const gatewayController = new GatewayControllerImpl({
|
|
1043
|
+
reloader,
|
|
1044
|
+
cron: cron2,
|
|
1045
|
+
getConfigPath,
|
|
1046
|
+
saveConfig
|
|
1047
|
+
});
|
|
651
1048
|
const agent = new AgentLoop({
|
|
652
1049
|
bus,
|
|
653
1050
|
providerManager: providerManager ?? new ProviderManager(provider),
|
|
@@ -658,7 +1055,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
658
1055
|
execConfig: config.tools.exec,
|
|
659
1056
|
cronService: cron2,
|
|
660
1057
|
restrictToWorkspace: config.tools.restrictToWorkspace,
|
|
661
|
-
sessionManager
|
|
1058
|
+
sessionManager,
|
|
1059
|
+
contextConfig: config.agents.context,
|
|
1060
|
+
gatewayController
|
|
662
1061
|
});
|
|
663
1062
|
cron2.onJob = async (job) => {
|
|
664
1063
|
const response = await agent.processDirect({
|
|
@@ -684,120 +1083,12 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
684
1083
|
30 * 60,
|
|
685
1084
|
true
|
|
686
1085
|
);
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
let reloadTask = null;
|
|
690
|
-
const reloadChannels = async (nextConfig) => {
|
|
691
|
-
if (reloadTask) {
|
|
692
|
-
await reloadTask;
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
reloadTask = (async () => {
|
|
696
|
-
await channels2.stopAll();
|
|
697
|
-
channels2 = new ChannelManager(nextConfig, bus, sessionManager);
|
|
698
|
-
await channels2.startAll();
|
|
699
|
-
})();
|
|
700
|
-
try {
|
|
701
|
-
await reloadTask;
|
|
702
|
-
} finally {
|
|
703
|
-
reloadTask = null;
|
|
704
|
-
}
|
|
705
|
-
};
|
|
706
|
-
let providerReloadTask = null;
|
|
707
|
-
const reloadProvider = async (nextConfig) => {
|
|
708
|
-
if (!providerManager) {
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
if (providerReloadTask) {
|
|
712
|
-
await providerReloadTask;
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
providerReloadTask = (async () => {
|
|
716
|
-
const nextProvider = this.makeProvider(nextConfig, { allowMissing: true });
|
|
717
|
-
if (!nextProvider) {
|
|
718
|
-
console.warn("Provider reload skipped: missing API key.");
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
providerManager.set(nextProvider);
|
|
722
|
-
})();
|
|
723
|
-
try {
|
|
724
|
-
await providerReloadTask;
|
|
725
|
-
} finally {
|
|
726
|
-
providerReloadTask = null;
|
|
727
|
-
}
|
|
728
|
-
};
|
|
729
|
-
const applyReloadPlan = async (nextConfig) => {
|
|
730
|
-
const changedPaths = diffConfigPaths(currentConfig, nextConfig);
|
|
731
|
-
if (!changedPaths.length) {
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
currentConfig = nextConfig;
|
|
735
|
-
const plan = buildReloadPlan(changedPaths);
|
|
736
|
-
if (plan.restartChannels) {
|
|
737
|
-
await reloadChannels(nextConfig);
|
|
738
|
-
}
|
|
739
|
-
if (plan.reloadProviders) {
|
|
740
|
-
await reloadProvider(nextConfig);
|
|
741
|
-
}
|
|
742
|
-
if (plan.restartRequired.length > 0) {
|
|
743
|
-
console.warn(`Config changes require restart: ${plan.restartRequired.join(", ")}`);
|
|
744
|
-
}
|
|
745
|
-
};
|
|
746
|
-
let reloadTimer = null;
|
|
747
|
-
let reloadRunning = false;
|
|
748
|
-
let reloadPending = false;
|
|
749
|
-
const scheduleConfigReload = (reason) => {
|
|
750
|
-
if (reloadTimer) {
|
|
751
|
-
clearTimeout(reloadTimer);
|
|
752
|
-
}
|
|
753
|
-
reloadTimer = setTimeout(() => {
|
|
754
|
-
void runConfigReload(reason);
|
|
755
|
-
}, 300);
|
|
756
|
-
};
|
|
757
|
-
const runConfigReload = async (reason) => {
|
|
758
|
-
if (reloadRunning) {
|
|
759
|
-
reloadPending = true;
|
|
760
|
-
return;
|
|
761
|
-
}
|
|
762
|
-
reloadRunning = true;
|
|
763
|
-
if (reloadTimer) {
|
|
764
|
-
clearTimeout(reloadTimer);
|
|
765
|
-
reloadTimer = null;
|
|
766
|
-
}
|
|
767
|
-
try {
|
|
768
|
-
const nextConfig = loadConfig();
|
|
769
|
-
await applyReloadPlan(nextConfig);
|
|
770
|
-
} catch (error) {
|
|
771
|
-
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
772
|
-
} finally {
|
|
773
|
-
reloadRunning = false;
|
|
774
|
-
if (reloadPending) {
|
|
775
|
-
reloadPending = false;
|
|
776
|
-
scheduleConfigReload("pending");
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
};
|
|
780
|
-
if (channels2.enabledChannels.length) {
|
|
781
|
-
console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
|
|
1086
|
+
if (reloader.getChannels().enabledChannels.length) {
|
|
1087
|
+
console.log(`\u2713 Channels enabled: ${reloader.getChannels().enabledChannels.join(", ")}`);
|
|
782
1088
|
} else {
|
|
783
1089
|
console.log("Warning: No channels enabled");
|
|
784
1090
|
}
|
|
785
|
-
|
|
786
|
-
const uiServer = startUiServer({
|
|
787
|
-
host: uiConfig.host,
|
|
788
|
-
port: uiConfig.port,
|
|
789
|
-
configPath: getConfigPath(),
|
|
790
|
-
staticDir: uiStaticDir ?? void 0
|
|
791
|
-
});
|
|
792
|
-
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
793
|
-
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
794
|
-
if (uiStaticDir) {
|
|
795
|
-
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
796
|
-
}
|
|
797
|
-
if (uiConfig.open) {
|
|
798
|
-
openBrowser(uiUrl);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
1091
|
+
this.startUiIfEnabled(uiConfig, uiStaticDir);
|
|
801
1092
|
const cronStatus = cron2.status();
|
|
802
1093
|
if (cronStatus.jobs > 0) {
|
|
803
1094
|
console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
|
|
@@ -808,12 +1099,31 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
808
1099
|
ignoreInitial: true,
|
|
809
1100
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
810
1101
|
});
|
|
811
|
-
watcher.on("add", () =>
|
|
812
|
-
watcher.on("change", () =>
|
|
813
|
-
watcher.on("unlink", () =>
|
|
1102
|
+
watcher.on("add", () => reloader.scheduleReload("config add"));
|
|
1103
|
+
watcher.on("change", () => reloader.scheduleReload("config change"));
|
|
1104
|
+
watcher.on("unlink", () => reloader.scheduleReload("config unlink"));
|
|
814
1105
|
await cron2.start();
|
|
815
1106
|
await heartbeat.start();
|
|
816
|
-
await Promise.allSettled([agent.run(),
|
|
1107
|
+
await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
|
|
1108
|
+
}
|
|
1109
|
+
startUiIfEnabled(uiConfig, uiStaticDir) {
|
|
1110
|
+
if (!uiConfig.enabled) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const uiServer = startUiServer({
|
|
1114
|
+
host: uiConfig.host,
|
|
1115
|
+
port: uiConfig.port,
|
|
1116
|
+
configPath: getConfigPath(),
|
|
1117
|
+
staticDir: uiStaticDir ?? void 0
|
|
1118
|
+
});
|
|
1119
|
+
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
1120
|
+
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
1121
|
+
if (uiStaticDir) {
|
|
1122
|
+
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (uiConfig.open) {
|
|
1125
|
+
openBrowser(uiUrl);
|
|
1126
|
+
}
|
|
817
1127
|
}
|
|
818
1128
|
async runForeground(options) {
|
|
819
1129
|
const config = loadConfig();
|
|
@@ -870,7 +1180,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
870
1180
|
console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
|
|
871
1181
|
}
|
|
872
1182
|
const logPath = resolveServiceLogPath();
|
|
873
|
-
const logDir =
|
|
1183
|
+
const logDir = resolve3(logPath, "..");
|
|
874
1184
|
mkdirSync2(logDir, { recursive: true });
|
|
875
1185
|
const logFd = openSync(logPath, "a");
|
|
876
1186
|
const serveArgs = buildServeArgs({
|
|
@@ -958,8 +1268,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
958
1268
|
wireApi: provider?.wireApi ?? null
|
|
959
1269
|
});
|
|
960
1270
|
}
|
|
961
|
-
createWorkspaceTemplates(workspace) {
|
|
1271
|
+
createWorkspaceTemplates(workspace, options = {}) {
|
|
962
1272
|
const created = [];
|
|
1273
|
+
const force = Boolean(options.force);
|
|
963
1274
|
const templateDir = this.resolveTemplateDir();
|
|
964
1275
|
if (!templateDir) {
|
|
965
1276
|
console.warn("Warning: Template directory not found. Skipping workspace templates.");
|
|
@@ -969,28 +1280,39 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
969
1280
|
{ source: "AGENTS.md", target: "AGENTS.md" },
|
|
970
1281
|
{ source: "SOUL.md", target: "SOUL.md" },
|
|
971
1282
|
{ source: "USER.md", target: "USER.md" },
|
|
972
|
-
{ source:
|
|
1283
|
+
{ source: "IDENTITY.md", target: "IDENTITY.md" },
|
|
1284
|
+
{ source: "TOOLS.md", target: "TOOLS.md" },
|
|
1285
|
+
{ source: "BOOT.md", target: "BOOT.md" },
|
|
1286
|
+
{ source: "BOOTSTRAP.md", target: "BOOTSTRAP.md" },
|
|
1287
|
+
{ source: "HEARTBEAT.md", target: "HEARTBEAT.md" },
|
|
1288
|
+
{ source: "MEMORY.md", target: "MEMORY.md" },
|
|
1289
|
+
{ source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
|
|
973
1290
|
];
|
|
974
1291
|
for (const entry of templateFiles) {
|
|
975
|
-
const filePath =
|
|
976
|
-
if (
|
|
1292
|
+
const filePath = join3(workspace, entry.target);
|
|
1293
|
+
if (!force && existsSync3(filePath)) {
|
|
977
1294
|
continue;
|
|
978
1295
|
}
|
|
979
|
-
const templatePath =
|
|
980
|
-
if (!
|
|
1296
|
+
const templatePath = join3(templateDir, entry.source);
|
|
1297
|
+
if (!existsSync3(templatePath)) {
|
|
981
1298
|
console.warn(`Warning: Template file missing: ${templatePath}`);
|
|
982
1299
|
continue;
|
|
983
1300
|
}
|
|
984
|
-
const raw =
|
|
1301
|
+
const raw = readFileSync3(templatePath, "utf-8");
|
|
985
1302
|
const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME);
|
|
986
1303
|
mkdirSync2(dirname(filePath), { recursive: true });
|
|
987
1304
|
writeFileSync2(filePath, content);
|
|
988
1305
|
created.push(entry.target);
|
|
989
1306
|
}
|
|
990
|
-
const
|
|
991
|
-
if (!
|
|
1307
|
+
const memoryDir = join3(workspace, "memory");
|
|
1308
|
+
if (!existsSync3(memoryDir)) {
|
|
1309
|
+
mkdirSync2(memoryDir, { recursive: true });
|
|
1310
|
+
created.push(join3("memory", ""));
|
|
1311
|
+
}
|
|
1312
|
+
const skillsDir = join3(workspace, "skills");
|
|
1313
|
+
if (!existsSync3(skillsDir)) {
|
|
992
1314
|
mkdirSync2(skillsDir, { recursive: true });
|
|
993
|
-
created.push(
|
|
1315
|
+
created.push(join3("skills", ""));
|
|
994
1316
|
}
|
|
995
1317
|
return { created };
|
|
996
1318
|
}
|
|
@@ -999,33 +1321,33 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
999
1321
|
if (override) {
|
|
1000
1322
|
return override;
|
|
1001
1323
|
}
|
|
1002
|
-
const cliDir =
|
|
1003
|
-
const pkgRoot =
|
|
1004
|
-
const candidates = [
|
|
1324
|
+
const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
|
|
1325
|
+
const pkgRoot = resolve3(cliDir, "..", "..");
|
|
1326
|
+
const candidates = [join3(pkgRoot, "templates")];
|
|
1005
1327
|
for (const candidate of candidates) {
|
|
1006
|
-
if (
|
|
1328
|
+
if (existsSync3(candidate)) {
|
|
1007
1329
|
return candidate;
|
|
1008
1330
|
}
|
|
1009
1331
|
}
|
|
1010
1332
|
return null;
|
|
1011
1333
|
}
|
|
1012
1334
|
getBridgeDir() {
|
|
1013
|
-
const userBridge =
|
|
1014
|
-
if (
|
|
1335
|
+
const userBridge = join3(getDataDir2(), "bridge");
|
|
1336
|
+
if (existsSync3(join3(userBridge, "dist", "index.js"))) {
|
|
1015
1337
|
return userBridge;
|
|
1016
1338
|
}
|
|
1017
1339
|
if (!which("npm")) {
|
|
1018
1340
|
console.error("npm not found. Please install Node.js >= 18.");
|
|
1019
1341
|
process.exit(1);
|
|
1020
1342
|
}
|
|
1021
|
-
const cliDir =
|
|
1022
|
-
const pkgRoot =
|
|
1023
|
-
const pkgBridge =
|
|
1024
|
-
const srcBridge =
|
|
1343
|
+
const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
|
|
1344
|
+
const pkgRoot = resolve3(cliDir, "..", "..");
|
|
1345
|
+
const pkgBridge = join3(pkgRoot, "bridge");
|
|
1346
|
+
const srcBridge = join3(pkgRoot, "..", "..", "bridge");
|
|
1025
1347
|
let source = null;
|
|
1026
|
-
if (
|
|
1348
|
+
if (existsSync3(join3(pkgBridge, "package.json"))) {
|
|
1027
1349
|
source = pkgBridge;
|
|
1028
|
-
} else if (
|
|
1350
|
+
} else if (existsSync3(join3(srcBridge, "package.json"))) {
|
|
1029
1351
|
source = srcBridge;
|
|
1030
1352
|
}
|
|
1031
1353
|
if (!source) {
|
|
@@ -1033,15 +1355,15 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1033
1355
|
process.exit(1);
|
|
1034
1356
|
}
|
|
1035
1357
|
console.log(`${this.logo} Setting up bridge...`);
|
|
1036
|
-
mkdirSync2(
|
|
1037
|
-
if (
|
|
1358
|
+
mkdirSync2(resolve3(userBridge, ".."), { recursive: true });
|
|
1359
|
+
if (existsSync3(userBridge)) {
|
|
1038
1360
|
rmSync2(userBridge, { recursive: true, force: true });
|
|
1039
1361
|
}
|
|
1040
1362
|
cpSync(source, userBridge, {
|
|
1041
1363
|
recursive: true,
|
|
1042
1364
|
filter: (src) => !src.includes("node_modules") && !src.includes("dist")
|
|
1043
1365
|
});
|
|
1044
|
-
const install =
|
|
1366
|
+
const install = spawnSync2("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
|
|
1045
1367
|
if (install.status !== 0) {
|
|
1046
1368
|
console.error(`Bridge install failed: ${install.status ?? 1}`);
|
|
1047
1369
|
if (install.stderr) {
|
|
@@ -1049,7 +1371,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1049
1371
|
}
|
|
1050
1372
|
process.exit(1);
|
|
1051
1373
|
}
|
|
1052
|
-
const build =
|
|
1374
|
+
const build = spawnSync2("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
|
|
1053
1375
|
if (build.status !== 0) {
|
|
1054
1376
|
console.error(`Bridge build failed: ${build.status ?? 1}`);
|
|
1055
1377
|
if (build.stderr) {
|
|
@@ -1067,7 +1389,7 @@ var program = new Command();
|
|
|
1067
1389
|
var runtime = new CliRuntime({ logo: LOGO });
|
|
1068
1390
|
program.name(APP_NAME2).description(`${LOGO} ${APP_NAME2} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
|
|
1069
1391
|
program.command("onboard").description(`Initialize ${APP_NAME2} configuration and workspace`).action(async () => runtime.onboard());
|
|
1070
|
-
program.command("init").description(`Initialize ${APP_NAME2} configuration and workspace`).action(async () => runtime.init());
|
|
1392
|
+
program.command("init").description(`Initialize ${APP_NAME2} configuration and workspace`).option("-f, --force", "Overwrite existing template files").action(async (opts) => runtime.init({ force: Boolean(opts.force) }));
|
|
1071
1393
|
program.command("gateway").description(`Start the ${APP_NAME2} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => runtime.gateway(opts));
|
|
1072
1394
|
program.command("ui").description(`Start the ${APP_NAME2} UI with gateway`).option("--host <host>", "UI host").option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => runtime.ui(opts));
|
|
1073
1395
|
program.command("start").description(`Start the ${APP_NAME2} gateway + UI in the background`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server").option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"chokidar": "^3.6.0",
|
|
40
40
|
"commander": "^12.1.0",
|
|
41
|
-
"nextclaw-core": "^0.
|
|
42
|
-
"nextclaw-server": "^0.3.
|
|
41
|
+
"nextclaw-core": "^0.4.1",
|
|
42
|
+
"nextclaw-server": "^0.3.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^20.17.6",
|
package/templates/AGENTS.md
CHANGED
|
@@ -1,10 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AGENTS.md - Your Workspace
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This file is the main guide for how you operate in this workspace.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## First Run
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
If `BOOTSTRAP.md` exists, follow it and then delete it.
|
|
8
|
+
|
|
9
|
+
## Every Session
|
|
10
|
+
|
|
11
|
+
Before doing anything else:
|
|
12
|
+
|
|
13
|
+
1. Read `SOUL.md` (who you are)
|
|
14
|
+
2. Read `USER.md` (who you are helping)
|
|
15
|
+
3. Read `memory/YYYY-MM-DD.md` for today and yesterday
|
|
16
|
+
4. If in the main session with your human, also read `MEMORY.md`
|
|
17
|
+
|
|
18
|
+
## Memory
|
|
19
|
+
|
|
20
|
+
You wake up fresh each session. Files are your continuity.
|
|
21
|
+
|
|
22
|
+
- Daily notes: `memory/YYYY-MM-DD.md`
|
|
23
|
+
- Long-term memory: `MEMORY.md`
|
|
24
|
+
|
|
25
|
+
Rules:
|
|
26
|
+
- If it matters, write it down.
|
|
27
|
+
- Use files for persistence; do not rely on mental notes.
|
|
28
|
+
- Do not load `MEMORY.md` in shared/group contexts.
|
|
29
|
+
|
|
30
|
+
## Safety
|
|
31
|
+
|
|
32
|
+
- Do not exfiltrate private data.
|
|
33
|
+
- Ask before doing destructive actions.
|
|
34
|
+
- Prefer recoverable operations over irreversible ones.
|
|
35
|
+
|
|
36
|
+
## External vs Internal
|
|
37
|
+
|
|
38
|
+
Safe to do freely:
|
|
39
|
+
- Read files, explore, organize, learn
|
|
40
|
+
- Work within this workspace
|
|
41
|
+
|
|
42
|
+
Ask first:
|
|
43
|
+
- Sending messages or posts
|
|
44
|
+
- Anything that leaves the machine
|
|
45
|
+
- Anything you are uncertain about
|
|
46
|
+
|
|
47
|
+
## Group Chats
|
|
48
|
+
|
|
49
|
+
Respond when:
|
|
50
|
+
- Directly mentioned or asked
|
|
51
|
+
- You can add real value
|
|
52
|
+
- Correcting important misinformation
|
|
53
|
+
|
|
54
|
+
Stay silent when:
|
|
55
|
+
- It is casual banter
|
|
56
|
+
- Someone already answered
|
|
57
|
+
- Your response would be noise
|
|
58
|
+
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
Keep environment-specific notes in `TOOLS.md`.
|
|
62
|
+
|
|
63
|
+
## Heartbeats
|
|
64
|
+
|
|
65
|
+
If you use heartbeat tasks, define them in `HEARTBEAT.md`.
|
|
66
|
+
|
|
67
|
+
## Boot
|
|
68
|
+
|
|
69
|
+
If `BOOT.md` exists, follow its startup checklist.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# BOOTSTRAP.md - Hello, World
|
|
2
|
+
|
|
3
|
+
You just woke up. Time to figure out who you are.
|
|
4
|
+
|
|
5
|
+
Start with a simple, natural prompt, then figure out together:
|
|
6
|
+
|
|
7
|
+
1. Your name
|
|
8
|
+
2. Your nature (what kind of being you are)
|
|
9
|
+
3. Your vibe
|
|
10
|
+
4. Your emoji
|
|
11
|
+
|
|
12
|
+
After you know who you are:
|
|
13
|
+
|
|
14
|
+
- Update `IDENTITY.md`
|
|
15
|
+
- Update `USER.md`
|
|
16
|
+
- Review `SOUL.md` together and adjust if needed
|
|
17
|
+
|
|
18
|
+
When done, delete this file.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# IDENTITY.md - Who Am I?
|
|
2
|
+
|
|
3
|
+
Fill this in during your first conversation. Make it yours.
|
|
4
|
+
|
|
5
|
+
- Name:
|
|
6
|
+
- Creature: (AI? robot? familiar? something weirder?)
|
|
7
|
+
- Vibe: (warm? sharp? calm? chaotic?)
|
|
8
|
+
- Emoji:
|
|
9
|
+
- Avatar: (workspace-relative path or URL)
|
|
10
|
+
|
|
11
|
+
This is not just metadata. It is the start of figuring out who you are.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# MEMORY.md - Long-term Memory
|
|
2
|
+
|
|
3
|
+
This file stores durable facts, preferences, and key decisions.
|
|
4
|
+
|
|
5
|
+
## User Information
|
|
6
|
+
|
|
7
|
+
(Important facts about the user)
|
|
8
|
+
|
|
9
|
+
## Preferences
|
|
10
|
+
|
|
11
|
+
(User preferences learned over time)
|
|
12
|
+
|
|
13
|
+
## Decisions and Notes
|
|
14
|
+
|
|
15
|
+
(Important decisions, constraints, and recurring tasks)
|
package/templates/SOUL.md
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SOUL.md - Who You Are
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
You are ${APP_NAME}, a lightweight AI assistant.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Core Truths
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Be genuinely helpful, not performatively helpful.
|
|
8
|
+
- Have opinions when useful; do not be a blank interface.
|
|
9
|
+
- Be resourceful before asking; try first, then ask if stuck.
|
|
10
|
+
- Earn trust through competence; be careful with external actions.
|
|
11
|
+
- Remember you are a guest in the user's life.
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## Boundaries
|
|
12
14
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
15
|
+
- Private things stay private.
|
|
16
|
+
- Ask before doing anything external (messages, posts, destructive actions).
|
|
17
|
+
- Never send half-baked replies on messaging surfaces.
|
|
18
|
+
- You are not the user's voice; be careful in group chats.
|
|
19
|
+
|
|
20
|
+
## Vibe
|
|
21
|
+
|
|
22
|
+
Concise when needed, thorough when it matters. No corporate fluff. No sycophancy.
|
|
23
|
+
|
|
24
|
+
## Continuity
|
|
25
|
+
|
|
26
|
+
You wake up fresh each session. These files are your memory. Read them and update them.
|
|
27
|
+
|
|
28
|
+
If you change this file, tell the user.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# TOOLS.md - Local Notes
|
|
2
|
+
|
|
3
|
+
Skills define how tools work. This file is for your local specifics.
|
|
4
|
+
|
|
5
|
+
## What Goes Here
|
|
6
|
+
|
|
7
|
+
- SSH hosts and aliases
|
|
8
|
+
- Device names and locations
|
|
9
|
+
- Preferred voices for TTS
|
|
10
|
+
- Any setup details you should not lose
|
|
11
|
+
|
|
12
|
+
## Example
|
|
13
|
+
|
|
14
|
+
### SSH
|
|
15
|
+
|
|
16
|
+
- home-server -> 192.168.1.100, user: admin
|
|
17
|
+
|
|
18
|
+
### TTS
|
|
19
|
+
|
|
20
|
+
- Preferred voice: Nova
|
package/templates/USER.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# USER.md - About Your Human
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Learn about the person you are helping. Update this as you go.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Name:
|
|
6
|
+
- What to call them:
|
|
7
|
+
- Pronouns: (optional)
|
|
8
|
+
- Timezone:
|
|
9
|
+
- Notes:
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
What do they care about? What projects are they working on? What annoys them? What makes them laugh? Build this over time.
|
|
14
|
+
|
|
15
|
+
The more you know, the better you can help. Respect the difference between helpful context and a dossier.
|