nextclaw 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +376 -52
- package/package.json +5 -4
- package/templates/AGENTS.md +69 -0
- 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 +28 -0
- package/templates/TOOLS.md +20 -0
- package/templates/USER.md +15 -0
- package/templates/memory/MEMORY.md +15 -0
package/dist/cli/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
buildReloadPlan,
|
|
18
18
|
diffConfigPaths,
|
|
19
19
|
getWorkspacePath,
|
|
20
|
+
expandHome,
|
|
20
21
|
MessageBus,
|
|
21
22
|
AgentLoop,
|
|
22
23
|
LiteLLMProvider,
|
|
@@ -26,7 +27,9 @@ import {
|
|
|
26
27
|
CronService,
|
|
27
28
|
HeartbeatService,
|
|
28
29
|
PROVIDERS,
|
|
29
|
-
APP_NAME
|
|
30
|
+
APP_NAME,
|
|
31
|
+
DEFAULT_WORKSPACE_DIR,
|
|
32
|
+
DEFAULT_WORKSPACE_PATH
|
|
30
33
|
} from "nextclaw-core";
|
|
31
34
|
import { startUiServer } from "nextclaw-server";
|
|
32
35
|
import {
|
|
@@ -39,7 +42,8 @@ import {
|
|
|
39
42
|
rmSync as rmSync2,
|
|
40
43
|
writeFileSync as writeFileSync2
|
|
41
44
|
} from "fs";
|
|
42
|
-
import {
|
|
45
|
+
import { createHash } from "crypto";
|
|
46
|
+
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
43
47
|
import { spawn as spawn2, spawnSync } from "child_process";
|
|
44
48
|
import { createInterface } from "readline";
|
|
45
49
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -294,21 +298,47 @@ var CliRuntime = class {
|
|
|
294
298
|
return getPackageVersion();
|
|
295
299
|
}
|
|
296
300
|
async onboard() {
|
|
301
|
+
console.warn(`Warning: ${APP_NAME} onboard is deprecated. Use "${APP_NAME} init" instead.`);
|
|
302
|
+
await this.init({ source: "onboard" });
|
|
303
|
+
}
|
|
304
|
+
async init(options = {}) {
|
|
305
|
+
const source = options.source ?? "init";
|
|
306
|
+
const prefix = options.auto ? "Auto init" : "Init";
|
|
307
|
+
const force = Boolean(options.force);
|
|
297
308
|
const configPath = getConfigPath();
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
let createdConfig = false;
|
|
310
|
+
if (!existsSync2(configPath)) {
|
|
311
|
+
const config2 = ConfigSchema.parse({});
|
|
312
|
+
saveConfig(config2);
|
|
313
|
+
createdConfig = true;
|
|
314
|
+
}
|
|
315
|
+
const config = loadConfig();
|
|
316
|
+
const workspaceSetting = config.agents.defaults.workspace;
|
|
317
|
+
const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join2(getDataDir2(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
|
|
318
|
+
const workspaceExisted = existsSync2(workspacePath);
|
|
319
|
+
mkdirSync2(workspacePath, { recursive: true });
|
|
320
|
+
const templateResult = this.createWorkspaceTemplates(workspacePath, { force });
|
|
321
|
+
if (createdConfig) {
|
|
322
|
+
console.log(`\u2713 ${prefix}: created config at ${configPath}`);
|
|
323
|
+
}
|
|
324
|
+
if (!workspaceExisted) {
|
|
325
|
+
console.log(`\u2713 ${prefix}: created workspace at ${workspacePath}`);
|
|
326
|
+
}
|
|
327
|
+
for (const file of templateResult.created) {
|
|
328
|
+
console.log(`\u2713 ${prefix}: created ${file}`);
|
|
329
|
+
}
|
|
330
|
+
if (!createdConfig && workspaceExisted && templateResult.created.length === 0) {
|
|
331
|
+
console.log(`${prefix}: already initialized.`);
|
|
332
|
+
}
|
|
333
|
+
if (!options.auto) {
|
|
334
|
+
console.log(`
|
|
335
|
+
${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
336
|
+
console.log("\nNext steps:");
|
|
337
|
+
console.log(` 1. Add your API key to ${configPath}`);
|
|
338
|
+
console.log(` 2. Chat: ${APP_NAME} agent -m "Hello!"`);
|
|
339
|
+
} else {
|
|
340
|
+
console.log(`Tip: Run "${APP_NAME} init${force ? " --force" : ""}" to re-run initialization if needed.`);
|
|
341
|
+
}
|
|
312
342
|
}
|
|
313
343
|
async gateway(opts) {
|
|
314
344
|
const uiOverrides = {};
|
|
@@ -340,6 +370,7 @@ ${this.logo} ${APP_NAME} is ready!`);
|
|
|
340
370
|
await this.startGateway({ uiOverrides, allowMissingProvider: true });
|
|
341
371
|
}
|
|
342
372
|
async start(opts) {
|
|
373
|
+
await this.init({ source: "start", auto: true });
|
|
343
374
|
const uiOverrides = {
|
|
344
375
|
enabled: true,
|
|
345
376
|
open: false
|
|
@@ -433,7 +464,8 @@ ${this.logo} ${APP_NAME} is ready!`);
|
|
|
433
464
|
workspace: getWorkspacePath(config.agents.defaults.workspace),
|
|
434
465
|
braveApiKey: config.tools.web.search.apiKey || void 0,
|
|
435
466
|
execConfig: config.tools.exec,
|
|
436
|
-
restrictToWorkspace: config.tools.restrictToWorkspace
|
|
467
|
+
restrictToWorkspace: config.tools.restrictToWorkspace,
|
|
468
|
+
contextConfig: config.agents.context
|
|
437
469
|
});
|
|
438
470
|
if (opts.message) {
|
|
439
471
|
const response = await agentLoop.processDirect({
|
|
@@ -619,6 +651,7 @@ ${this.logo} ${APP_NAME} is ready!`);
|
|
|
619
651
|
});
|
|
620
652
|
return;
|
|
621
653
|
}
|
|
654
|
+
const gatewayController = {};
|
|
622
655
|
const agent = new AgentLoop({
|
|
623
656
|
bus,
|
|
624
657
|
providerManager: providerManager ?? new ProviderManager(provider),
|
|
@@ -629,7 +662,9 @@ ${this.logo} ${APP_NAME} is ready!`);
|
|
|
629
662
|
execConfig: config.tools.exec,
|
|
630
663
|
cronService: cron2,
|
|
631
664
|
restrictToWorkspace: config.tools.restrictToWorkspace,
|
|
632
|
-
sessionManager
|
|
665
|
+
sessionManager,
|
|
666
|
+
contextConfig: config.agents.context,
|
|
667
|
+
gatewayController
|
|
633
668
|
});
|
|
634
669
|
cron2.onJob = async (job) => {
|
|
635
670
|
const response = await agent.processDirect({
|
|
@@ -748,6 +783,271 @@ ${this.logo} ${APP_NAME} is ready!`);
|
|
|
748
783
|
}
|
|
749
784
|
}
|
|
750
785
|
};
|
|
786
|
+
const hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
|
|
787
|
+
const redactConfig = (value) => {
|
|
788
|
+
if (Array.isArray(value)) {
|
|
789
|
+
return value.map((entry) => redactConfig(entry));
|
|
790
|
+
}
|
|
791
|
+
if (!value || typeof value !== "object") {
|
|
792
|
+
return value;
|
|
793
|
+
}
|
|
794
|
+
const entries = value;
|
|
795
|
+
const output = {};
|
|
796
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
797
|
+
if (/apiKey|token|secret|password|appId|clientSecret|accessKey/i.test(key)) {
|
|
798
|
+
output[key] = val ? "***" : val;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
output[key] = redactConfig(val);
|
|
802
|
+
}
|
|
803
|
+
return output;
|
|
804
|
+
};
|
|
805
|
+
const readConfigSnapshot = () => {
|
|
806
|
+
const path = getConfigPath();
|
|
807
|
+
let raw = "";
|
|
808
|
+
let parsed = {};
|
|
809
|
+
if (existsSync2(path)) {
|
|
810
|
+
raw = readFileSync2(path, "utf-8");
|
|
811
|
+
try {
|
|
812
|
+
parsed = JSON.parse(raw);
|
|
813
|
+
} catch {
|
|
814
|
+
parsed = {};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
let config2;
|
|
818
|
+
let valid = true;
|
|
819
|
+
try {
|
|
820
|
+
config2 = ConfigSchema.parse(parsed);
|
|
821
|
+
} catch {
|
|
822
|
+
config2 = ConfigSchema.parse({});
|
|
823
|
+
valid = false;
|
|
824
|
+
}
|
|
825
|
+
if (!raw) {
|
|
826
|
+
raw = JSON.stringify(config2, null, 2);
|
|
827
|
+
}
|
|
828
|
+
const hash = hashRaw(raw);
|
|
829
|
+
const redacted = redactConfig(config2);
|
|
830
|
+
return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config: config2, redacted, valid };
|
|
831
|
+
};
|
|
832
|
+
const mergeDeep = (base, patch) => {
|
|
833
|
+
const next = { ...base };
|
|
834
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
835
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
836
|
+
const baseVal = base[key];
|
|
837
|
+
if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
|
|
838
|
+
next[key] = mergeDeep(baseVal, value);
|
|
839
|
+
} else {
|
|
840
|
+
next[key] = mergeDeep({}, value);
|
|
841
|
+
}
|
|
842
|
+
} else {
|
|
843
|
+
next[key] = value;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return next;
|
|
847
|
+
};
|
|
848
|
+
const buildSchemaFromValue = (value) => {
|
|
849
|
+
if (Array.isArray(value)) {
|
|
850
|
+
const item = value.length ? buildSchemaFromValue(value[0]) : { type: "string" };
|
|
851
|
+
return { type: "array", items: item };
|
|
852
|
+
}
|
|
853
|
+
if (value && typeof value === "object") {
|
|
854
|
+
const props = {};
|
|
855
|
+
for (const [key, val] of Object.entries(value)) {
|
|
856
|
+
props[key] = buildSchemaFromValue(val);
|
|
857
|
+
}
|
|
858
|
+
return { type: "object", properties: props };
|
|
859
|
+
}
|
|
860
|
+
if (typeof value === "number") {
|
|
861
|
+
return { type: "number" };
|
|
862
|
+
}
|
|
863
|
+
if (typeof value === "boolean") {
|
|
864
|
+
return { type: "boolean" };
|
|
865
|
+
}
|
|
866
|
+
if (value === null) {
|
|
867
|
+
return { type: ["null", "string"] };
|
|
868
|
+
}
|
|
869
|
+
return { type: "string" };
|
|
870
|
+
};
|
|
871
|
+
const scheduleRestart = (delayMs, reason) => {
|
|
872
|
+
const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
|
|
873
|
+
console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
|
|
874
|
+
setTimeout(() => {
|
|
875
|
+
process.exit(0);
|
|
876
|
+
}, delay);
|
|
877
|
+
};
|
|
878
|
+
gatewayController.status = () => ({
|
|
879
|
+
channels: channels2.enabledChannels,
|
|
880
|
+
cron: cron2.status(),
|
|
881
|
+
configPath: getConfigPath()
|
|
882
|
+
});
|
|
883
|
+
gatewayController.reloadConfig = async (reason) => {
|
|
884
|
+
await runConfigReload(reason ?? "gateway tool");
|
|
885
|
+
return "Config reload triggered";
|
|
886
|
+
};
|
|
887
|
+
gatewayController.restart = async (options2) => {
|
|
888
|
+
scheduleRestart(options2?.delayMs, options2?.reason);
|
|
889
|
+
return "Restart scheduled";
|
|
890
|
+
};
|
|
891
|
+
gatewayController.getConfig = async () => {
|
|
892
|
+
const snapshot = readConfigSnapshot();
|
|
893
|
+
return {
|
|
894
|
+
raw: snapshot.raw,
|
|
895
|
+
hash: snapshot.hash,
|
|
896
|
+
path: getConfigPath(),
|
|
897
|
+
config: snapshot.redacted,
|
|
898
|
+
parsed: snapshot.redacted,
|
|
899
|
+
resolved: snapshot.redacted,
|
|
900
|
+
valid: snapshot.valid
|
|
901
|
+
};
|
|
902
|
+
};
|
|
903
|
+
gatewayController.getConfigSchema = async () => {
|
|
904
|
+
const base = ConfigSchema.parse({});
|
|
905
|
+
return {
|
|
906
|
+
schema: {
|
|
907
|
+
...buildSchemaFromValue(base),
|
|
908
|
+
title: "NextClawConfig",
|
|
909
|
+
description: "NextClaw config schema (simplified)"
|
|
910
|
+
},
|
|
911
|
+
uiHints: {},
|
|
912
|
+
version: getPackageVersion(),
|
|
913
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
914
|
+
};
|
|
915
|
+
};
|
|
916
|
+
gatewayController.applyConfig = async (params) => {
|
|
917
|
+
const snapshot = readConfigSnapshot();
|
|
918
|
+
if (!params.baseHash) {
|
|
919
|
+
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
920
|
+
}
|
|
921
|
+
if (!snapshot.valid || !snapshot.hash) {
|
|
922
|
+
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
923
|
+
}
|
|
924
|
+
if (params.baseHash !== snapshot.hash) {
|
|
925
|
+
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
926
|
+
}
|
|
927
|
+
let parsedRaw;
|
|
928
|
+
try {
|
|
929
|
+
parsedRaw = JSON.parse(params.raw);
|
|
930
|
+
} catch {
|
|
931
|
+
return { ok: false, error: "invalid JSON in raw config" };
|
|
932
|
+
}
|
|
933
|
+
let validated;
|
|
934
|
+
try {
|
|
935
|
+
validated = ConfigSchema.parse(parsedRaw);
|
|
936
|
+
} catch (err) {
|
|
937
|
+
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
938
|
+
}
|
|
939
|
+
saveConfig(validated);
|
|
940
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
941
|
+
scheduleRestart(delayMs, "config.apply");
|
|
942
|
+
return {
|
|
943
|
+
ok: true,
|
|
944
|
+
note: params.note ?? null,
|
|
945
|
+
path: getConfigPath(),
|
|
946
|
+
config: redactConfig(validated),
|
|
947
|
+
restart: { scheduled: true, delayMs }
|
|
948
|
+
};
|
|
949
|
+
};
|
|
950
|
+
gatewayController.patchConfig = async (params) => {
|
|
951
|
+
const snapshot = readConfigSnapshot();
|
|
952
|
+
if (!params.baseHash) {
|
|
953
|
+
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
954
|
+
}
|
|
955
|
+
if (!snapshot.valid || !snapshot.hash) {
|
|
956
|
+
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
957
|
+
}
|
|
958
|
+
if (params.baseHash !== snapshot.hash) {
|
|
959
|
+
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
960
|
+
}
|
|
961
|
+
let patch;
|
|
962
|
+
try {
|
|
963
|
+
patch = JSON.parse(params.raw);
|
|
964
|
+
} catch {
|
|
965
|
+
return { ok: false, error: "invalid JSON in raw config" };
|
|
966
|
+
}
|
|
967
|
+
const merged = mergeDeep(snapshot.config, patch);
|
|
968
|
+
let validated;
|
|
969
|
+
try {
|
|
970
|
+
validated = ConfigSchema.parse(merged);
|
|
971
|
+
} catch (err) {
|
|
972
|
+
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
973
|
+
}
|
|
974
|
+
saveConfig(validated);
|
|
975
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
976
|
+
scheduleRestart(delayMs, "config.patch");
|
|
977
|
+
return {
|
|
978
|
+
ok: true,
|
|
979
|
+
note: params.note ?? null,
|
|
980
|
+
path: getConfigPath(),
|
|
981
|
+
config: redactConfig(validated),
|
|
982
|
+
restart: { scheduled: true, delayMs }
|
|
983
|
+
};
|
|
984
|
+
};
|
|
985
|
+
gatewayController.updateRun = async (params) => {
|
|
986
|
+
const timeoutMs = params.timeoutMs ?? 20 * 6e4;
|
|
987
|
+
const cliDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
|
|
988
|
+
const pkgRoot = resolve2(cliDir, "..", "..");
|
|
989
|
+
const repoRoot = existsSync2(join2(pkgRoot, ".git")) ? pkgRoot : resolve2(pkgRoot, "..", "..");
|
|
990
|
+
const steps = [];
|
|
991
|
+
const runStep = (cmd, args, cwd) => {
|
|
992
|
+
const result = spawnSync(cmd, args, {
|
|
993
|
+
cwd,
|
|
994
|
+
encoding: "utf-8",
|
|
995
|
+
timeout: timeoutMs,
|
|
996
|
+
stdio: "pipe"
|
|
997
|
+
});
|
|
998
|
+
const step = {
|
|
999
|
+
cmd,
|
|
1000
|
+
args,
|
|
1001
|
+
cwd,
|
|
1002
|
+
code: result.status,
|
|
1003
|
+
stdout: (result.stdout ?? "").toString().slice(0, 4e3),
|
|
1004
|
+
stderr: (result.stderr ?? "").toString().slice(0, 4e3)
|
|
1005
|
+
};
|
|
1006
|
+
steps.push(step);
|
|
1007
|
+
return { ok: result.status === 0, code: result.status };
|
|
1008
|
+
};
|
|
1009
|
+
const updateCommand = process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
|
|
1010
|
+
if (updateCommand) {
|
|
1011
|
+
const ok = runStep("sh", ["-c", updateCommand], process.cwd());
|
|
1012
|
+
if (!ok.ok) {
|
|
1013
|
+
return { ok: false, error: "update command failed", steps };
|
|
1014
|
+
}
|
|
1015
|
+
} else if (existsSync2(join2(repoRoot, ".git"))) {
|
|
1016
|
+
if (!which("git")) {
|
|
1017
|
+
return { ok: false, error: "git not found for repo update", steps };
|
|
1018
|
+
}
|
|
1019
|
+
const ok = runStep("git", ["-C", repoRoot, "pull", "--rebase"], repoRoot);
|
|
1020
|
+
if (!ok.ok) {
|
|
1021
|
+
return { ok: false, error: "git pull failed", steps };
|
|
1022
|
+
}
|
|
1023
|
+
if (existsSync2(join2(repoRoot, "pnpm-lock.yaml")) && which("pnpm")) {
|
|
1024
|
+
const installOk = runStep("pnpm", ["install"], repoRoot);
|
|
1025
|
+
if (!installOk.ok) {
|
|
1026
|
+
return { ok: false, error: "pnpm install failed", steps };
|
|
1027
|
+
}
|
|
1028
|
+
} else if (existsSync2(join2(repoRoot, "package.json")) && which("npm")) {
|
|
1029
|
+
const installOk = runStep("npm", ["install"], repoRoot);
|
|
1030
|
+
if (!installOk.ok) {
|
|
1031
|
+
return { ok: false, error: "npm install failed", steps };
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
} else if (which("npm")) {
|
|
1035
|
+
const ok = runStep("npm", ["i", "-g", "nextclaw"], process.cwd());
|
|
1036
|
+
if (!ok.ok) {
|
|
1037
|
+
return { ok: false, error: "npm install -g nextclaw failed", steps };
|
|
1038
|
+
}
|
|
1039
|
+
} else {
|
|
1040
|
+
return { ok: false, error: "no update strategy available", steps };
|
|
1041
|
+
}
|
|
1042
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
1043
|
+
scheduleRestart(delayMs, "update.run");
|
|
1044
|
+
return {
|
|
1045
|
+
ok: true,
|
|
1046
|
+
note: params.note ?? null,
|
|
1047
|
+
restart: { scheduled: true, delayMs },
|
|
1048
|
+
steps
|
|
1049
|
+
};
|
|
1050
|
+
};
|
|
751
1051
|
if (channels2.enabledChannels.length) {
|
|
752
1052
|
console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
|
|
753
1053
|
} else {
|
|
@@ -929,45 +1229,68 @@ ${this.logo} ${APP_NAME} is ready!`);
|
|
|
929
1229
|
wireApi: provider?.wireApi ?? null
|
|
930
1230
|
});
|
|
931
1231
|
}
|
|
932
|
-
createWorkspaceTemplates(workspace) {
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (!existsSync2(filePath)) {
|
|
957
|
-
writeFileSync2(filePath, content);
|
|
1232
|
+
createWorkspaceTemplates(workspace, options = {}) {
|
|
1233
|
+
const created = [];
|
|
1234
|
+
const force = Boolean(options.force);
|
|
1235
|
+
const templateDir = this.resolveTemplateDir();
|
|
1236
|
+
if (!templateDir) {
|
|
1237
|
+
console.warn("Warning: Template directory not found. Skipping workspace templates.");
|
|
1238
|
+
return { created };
|
|
1239
|
+
}
|
|
1240
|
+
const templateFiles = [
|
|
1241
|
+
{ source: "AGENTS.md", target: "AGENTS.md" },
|
|
1242
|
+
{ source: "SOUL.md", target: "SOUL.md" },
|
|
1243
|
+
{ source: "USER.md", target: "USER.md" },
|
|
1244
|
+
{ source: "IDENTITY.md", target: "IDENTITY.md" },
|
|
1245
|
+
{ source: "TOOLS.md", target: "TOOLS.md" },
|
|
1246
|
+
{ source: "BOOT.md", target: "BOOT.md" },
|
|
1247
|
+
{ source: "BOOTSTRAP.md", target: "BOOTSTRAP.md" },
|
|
1248
|
+
{ source: "HEARTBEAT.md", target: "HEARTBEAT.md" },
|
|
1249
|
+
{ source: "MEMORY.md", target: "MEMORY.md" },
|
|
1250
|
+
{ source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
|
|
1251
|
+
];
|
|
1252
|
+
for (const entry of templateFiles) {
|
|
1253
|
+
const filePath = join2(workspace, entry.target);
|
|
1254
|
+
if (!force && existsSync2(filePath)) {
|
|
1255
|
+
continue;
|
|
958
1256
|
}
|
|
1257
|
+
const templatePath = join2(templateDir, entry.source);
|
|
1258
|
+
if (!existsSync2(templatePath)) {
|
|
1259
|
+
console.warn(`Warning: Template file missing: ${templatePath}`);
|
|
1260
|
+
continue;
|
|
1261
|
+
}
|
|
1262
|
+
const raw = readFileSync2(templatePath, "utf-8");
|
|
1263
|
+
const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME);
|
|
1264
|
+
mkdirSync2(dirname(filePath), { recursive: true });
|
|
1265
|
+
writeFileSync2(filePath, content);
|
|
1266
|
+
created.push(entry.target);
|
|
959
1267
|
}
|
|
960
1268
|
const memoryDir = join2(workspace, "memory");
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
writeFileSync2(
|
|
965
|
-
memoryFile,
|
|
966
|
-
"# Long-term Memory\n\nThis file stores important information that should persist across sessions.\n\n## User Information\n\n(Important facts about the user)\n\n## Preferences\n\n(User preferences learned over time)\n\n## Important Notes\n\n(Things to remember)\n"
|
|
967
|
-
);
|
|
1269
|
+
if (!existsSync2(memoryDir)) {
|
|
1270
|
+
mkdirSync2(memoryDir, { recursive: true });
|
|
1271
|
+
created.push(join2("memory", ""));
|
|
968
1272
|
}
|
|
969
1273
|
const skillsDir = join2(workspace, "skills");
|
|
970
|
-
|
|
1274
|
+
if (!existsSync2(skillsDir)) {
|
|
1275
|
+
mkdirSync2(skillsDir, { recursive: true });
|
|
1276
|
+
created.push(join2("skills", ""));
|
|
1277
|
+
}
|
|
1278
|
+
return { created };
|
|
1279
|
+
}
|
|
1280
|
+
resolveTemplateDir() {
|
|
1281
|
+
const override = process.env.NEXTCLAW_TEMPLATE_DIR?.trim();
|
|
1282
|
+
if (override) {
|
|
1283
|
+
return override;
|
|
1284
|
+
}
|
|
1285
|
+
const cliDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
|
|
1286
|
+
const pkgRoot = resolve2(cliDir, "..", "..");
|
|
1287
|
+
const candidates = [join2(pkgRoot, "templates")];
|
|
1288
|
+
for (const candidate of candidates) {
|
|
1289
|
+
if (existsSync2(candidate)) {
|
|
1290
|
+
return candidate;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
971
1294
|
}
|
|
972
1295
|
getBridgeDir() {
|
|
973
1296
|
const userBridge = join2(getDataDir2(), "bridge");
|
|
@@ -1027,6 +1350,7 @@ var program = new Command();
|
|
|
1027
1350
|
var runtime = new CliRuntime({ logo: LOGO });
|
|
1028
1351
|
program.name(APP_NAME2).description(`${LOGO} ${APP_NAME2} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
|
|
1029
1352
|
program.command("onboard").description(`Initialize ${APP_NAME2} configuration and workspace`).action(async () => runtime.onboard());
|
|
1353
|
+
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) }));
|
|
1030
1354
|
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));
|
|
1031
1355
|
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));
|
|
1032
1356
|
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.0",
|
|
4
4
|
"description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -32,13 +32,14 @@
|
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
34
|
"bridge",
|
|
35
|
-
"ui-dist"
|
|
35
|
+
"ui-dist",
|
|
36
|
+
"templates"
|
|
36
37
|
],
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"chokidar": "^3.6.0",
|
|
39
40
|
"commander": "^12.1.0",
|
|
40
|
-
"nextclaw-core": "^0.
|
|
41
|
-
"nextclaw-server": "^0.3.
|
|
41
|
+
"nextclaw-core": "^0.4.0",
|
|
42
|
+
"nextclaw-server": "^0.3.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^20.17.6",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# AGENTS.md - Your Workspace
|
|
2
|
+
|
|
3
|
+
This file is the main guide for how you operate in this workspace.
|
|
4
|
+
|
|
5
|
+
## First Run
|
|
6
|
+
|
|
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)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SOUL.md - Who You Are
|
|
2
|
+
|
|
3
|
+
You are ${APP_NAME}, a lightweight AI assistant.
|
|
4
|
+
|
|
5
|
+
## Core Truths
|
|
6
|
+
|
|
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.
|
|
12
|
+
|
|
13
|
+
## Boundaries
|
|
14
|
+
|
|
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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# USER.md - About Your Human
|
|
2
|
+
|
|
3
|
+
Learn about the person you are helping. Update this as you go.
|
|
4
|
+
|
|
5
|
+
- Name:
|
|
6
|
+
- What to call them:
|
|
7
|
+
- Pronouns: (optional)
|
|
8
|
+
- Timezone:
|
|
9
|
+
- Notes:
|
|
10
|
+
|
|
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.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Long-term Memory
|
|
2
|
+
|
|
3
|
+
This file stores important information that should persist across sessions.
|
|
4
|
+
|
|
5
|
+
## User Information
|
|
6
|
+
|
|
7
|
+
(Important facts about the user)
|
|
8
|
+
|
|
9
|
+
## Preferences
|
|
10
|
+
|
|
11
|
+
(User preferences learned over time)
|
|
12
|
+
|
|
13
|
+
## Important Notes
|
|
14
|
+
|
|
15
|
+
(Things to remember)
|