@gajae-gateway/cli 0.1.4 → 0.1.6
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/list.d.ts +8 -2
- package/dist/list.d.ts.map +1 -1
- package/dist/main.d.ts +8 -2
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2080 -67
- package/dist/migrate/hermes.d.ts +24 -0
- package/dist/migrate/hermes.d.ts.map +1 -0
- package/dist/migrate/openclaw.d.ts +10 -0
- package/dist/migrate/openclaw.d.ts.map +1 -0
- package/dist/migrate/plan.d.ts +117 -0
- package/dist/migrate/plan.d.ts.map +1 -0
- package/dist/migrate/source-fs.d.ts +34 -0
- package/dist/migrate/source-fs.d.ts.map +1 -0
- package/dist/migrate.d.ts +15 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/restart-stack.d.ts.map +1 -1
- package/dist/services.d.ts.map +1 -1
- package/dist/setup.d.ts +130 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/update.d.ts +95 -0
- package/dist/update.d.ts.map +1 -0
- package/dist/upgrade.d.ts +40 -0
- package/dist/upgrade.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/list.ts +35 -10
- package/src/main.ts +65 -5
- package/src/migrate/hermes.ts +436 -0
- package/src/migrate/openclaw.ts +284 -0
- package/src/migrate/plan.ts +343 -0
- package/src/migrate/source-fs.ts +126 -0
- package/src/migrate.ts +158 -0
- package/src/restart-stack.ts +11 -4
- package/src/services.ts +8 -5
- package/src/setup.ts +606 -0
- package/src/update.ts +450 -0
- package/src/upgrade.ts +205 -0
package/src/list.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MonitorRecord } from "@gajae-gateway/protocol";
|
|
1
|
+
import type { MonitorRecord, MonitorScheduleProjection } from "@gajae-gateway/protocol";
|
|
2
2
|
import { originKey } from "@gajae-gateway/protocol";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -135,13 +135,31 @@ function triggerSummary(monitor: MonitorRecord): string {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
export
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
{ name: "
|
|
138
|
+
export interface MonitorListRow {
|
|
139
|
+
readonly monitor: MonitorRecord;
|
|
140
|
+
readonly schedule: MonitorScheduleProjection | null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const MONITOR_COLUMNS: readonly Column<MonitorListRow>[] = [
|
|
144
|
+
{ name: "id", value: (row) => row.monitor.monitorId.slice(0, 8) },
|
|
145
|
+
{ name: "name", value: (row) => row.monitor.name },
|
|
146
|
+
{ name: "schedule", value: (row) => triggerSummary(row.monitor) },
|
|
147
|
+
{ name: "timezone", value: (row) => row.schedule?.effectiveTimezone ?? null },
|
|
148
|
+
{
|
|
149
|
+
name: "nextFire",
|
|
150
|
+
value: (row) => {
|
|
151
|
+
const { monitor, schedule } = row;
|
|
152
|
+
if (monitor.trigger.kind !== "cron") return null;
|
|
153
|
+
if (!monitor.enabled) return "paused";
|
|
154
|
+
if (!schedule) return null;
|
|
155
|
+
if (!schedule.nextFireAt) return "never";
|
|
156
|
+
const utc = schedule.nextFireAt.utc.replace(/:\d{2}\.\d{3}Z$/, "Z");
|
|
157
|
+
return `${schedule.nextFireAt.local} / ${utc}`;
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{ name: "events", value: (row) => [...row.monitor.eventTypes] },
|
|
161
|
+
{ name: "target", value: (row) => (row.monitor.channelTarget ? originKey(row.monitor.channelTarget.origin) : null) },
|
|
162
|
+
{ name: "enabled", value: (row) => row.monitor.enabled },
|
|
145
163
|
];
|
|
146
164
|
|
|
147
165
|
export interface SessionListRow {
|
|
@@ -176,12 +194,19 @@ export function renderList<T>(
|
|
|
176
194
|
columns: readonly Column<T>[],
|
|
177
195
|
rows: readonly T[],
|
|
178
196
|
options: ListOptions,
|
|
179
|
-
envelope: {
|
|
197
|
+
envelope: {
|
|
198
|
+
readonly key: string;
|
|
199
|
+
readonly result: unknown;
|
|
200
|
+
/** Restore the gateway array-item shape when display rows are wrapped. */
|
|
201
|
+
readonly serializeRow?: (row: T) => unknown;
|
|
202
|
+
},
|
|
180
203
|
): string[] {
|
|
181
204
|
const entries = pageRows(rows, options);
|
|
182
205
|
const selected = selectColumns(columns, options.fields);
|
|
183
206
|
if (!options.json) return renderTable(selected, entries);
|
|
184
207
|
if (!options.fields && options.limit === undefined && options.offset === 0) return [JSON.stringify(envelope.result)];
|
|
185
|
-
const projected = options.fields
|
|
208
|
+
const projected = options.fields
|
|
209
|
+
? projectRows(selected, entries)
|
|
210
|
+
: entries.map((entry) => (envelope.serializeRow ? envelope.serializeRow(entry.row) : entry.row));
|
|
186
211
|
return [JSON.stringify({ ...(envelope.result as object), [envelope.key]: projected })];
|
|
187
212
|
}
|
package/src/main.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { isAbsolute, join } from "node:path";
|
|
|
5
5
|
import type {
|
|
6
6
|
MonitorEventRecord,
|
|
7
7
|
MonitorRecord,
|
|
8
|
+
MonitorScheduleProjection,
|
|
8
9
|
MonitorSpec,
|
|
9
10
|
OpsCycleResult,
|
|
10
11
|
WorkJobsResult,
|
|
@@ -21,11 +22,13 @@ import {
|
|
|
21
22
|
columnNames,
|
|
22
23
|
type ListOptions,
|
|
23
24
|
MONITOR_COLUMNS,
|
|
25
|
+
type MonitorListRow,
|
|
24
26
|
parseListOptions,
|
|
25
27
|
renderList,
|
|
26
28
|
SESSION_COLUMNS,
|
|
27
29
|
type SessionListRow,
|
|
28
30
|
} from "./list";
|
|
31
|
+
import { migrate, parseMigrateArgs } from "./migrate";
|
|
29
32
|
import {
|
|
30
33
|
effectiveRestartState,
|
|
31
34
|
type LaunchRestartOptions,
|
|
@@ -36,6 +39,9 @@ import {
|
|
|
36
39
|
runRestartStack,
|
|
37
40
|
} from "./restart-stack";
|
|
38
41
|
import { type InstallServicesOptions, installServices, type ServicePlatform, serviceUsage } from "./services";
|
|
42
|
+
import { type RunSetupOptions, runSetup } from "./setup";
|
|
43
|
+
import { parseUpdateArgs, renderUpdate, runUpdate, type UpdateDeps } from "./update";
|
|
44
|
+
import { performUpgrade } from "./upgrade";
|
|
39
45
|
|
|
40
46
|
export function socketPath(home = process.env.GAJAEWAY_HOME): string {
|
|
41
47
|
return `${home ?? `${process.env.HOME ?? "~"}/.gajaeway`}/gateway.sock`;
|
|
@@ -58,10 +64,13 @@ export const COMMANDS = [
|
|
|
58
64
|
"monitors",
|
|
59
65
|
"work",
|
|
60
66
|
"services",
|
|
67
|
+
"migrate",
|
|
68
|
+
"update",
|
|
69
|
+
"setup",
|
|
61
70
|
] as const;
|
|
62
71
|
|
|
63
72
|
export const CLI_USAGE =
|
|
64
|
-
"usage: gajaeway [--socket PATH] status|shutdown|chat|daemon run|sessions list [--json] [--fields a,b,c] [--limit N] [--offset N]|sessions inspect <originKey-or-index>|memory audit|memory search <query>|monitors ... (test <id> [--type T] [--payload J] [--wait[=SECONDS]])|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] <text>|work status <name>|work steer <name> <text>|work retire [--force] <name>|work retire --all-dead|work jobs|ops backup <path>|ops redeliver <deliveryId>|ops redeliver --since <iso>|ops cycle [--json]|ops integrity|ops restore <backupPath>|ops restart-stack [--status]|services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux] (work run waits for a response; caller timeout does not end the attempt)";
|
|
73
|
+
"usage: gajaeway [--socket PATH] status|shutdown|chat|daemon run|sessions list [--json] [--fields a,b,c] [--limit N] [--offset N]|sessions inspect <originKey-or-index>|memory audit|memory search <query>|monitors ... (test <id> [--type T] [--payload J] [--wait[=SECONDS]])|work run|start <name> [--cwd DIR] [--resume] [--model ID|--preset NAME] [--notify originKey (start only)] <text>|work status <name>|work steer <name> <text>|work retire [--force] <name>|work retire --all-dead|work jobs|ops backup <path>|ops redeliver <deliveryId>|ops redeliver --since <iso>|ops cycle [--json]|ops integrity|ops restore <backupPath>|ops restart-stack [--status]|services install|repair --bin-dir DIR [--launch-agents-dir DIR] [--unit-dir DIR] [--platform darwin|linux]|migrate [--source PATH] [--target PATH] [--dry-run]|update [--check] [--force] [--bin-dir DIR] [--no-restart]|setup [--from-env] [--adapters discord,slack,telegram] [--owner ID] [--discord-app-id ID]|setup --status (work run waits for a response; caller timeout does not end the attempt); cron timezone is an IANA zone and defaults to the gateway host's local timezone";
|
|
65
74
|
|
|
66
75
|
/** Usage errors exit 2, as `gajaeway-gateway` does; 1 stays a runtime failure. */
|
|
67
76
|
export const USAGE_EXIT_CODE = 2;
|
|
@@ -87,11 +96,15 @@ function gatewayHome(): string {
|
|
|
87
96
|
|
|
88
97
|
export interface MainOptions {
|
|
89
98
|
readonly services?: Pick<InstallServicesOptions, "loginPathRunner" | "writeFile">;
|
|
99
|
+
/** Test seams for `setup`: the platform fetch and the prompter. */
|
|
100
|
+
readonly setup?: Pick<RunSetupOptions, "fetch" | "prompter" | "env">;
|
|
90
101
|
/** Test seams for `ops restart-stack`; the real path spawns the service manager. */
|
|
91
102
|
readonly restartStack?: {
|
|
92
103
|
readonly launch?: Omit<LaunchRestartOptions, "home">;
|
|
93
104
|
readonly run?: Omit<RunRestartOptions, "home" | "id">;
|
|
94
105
|
};
|
|
106
|
+
/** Test seams for `update`; the real path hits GitHub and the service manager. */
|
|
107
|
+
readonly update?: UpdateDeps;
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
export type ServicesAction = "install" | "repair";
|
|
@@ -589,7 +602,28 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
589
602
|
break;
|
|
590
603
|
}
|
|
591
604
|
case "ops": {
|
|
592
|
-
const [command, path] = parsed.rest;
|
|
605
|
+
const [command, path, ...extraArgs] = parsed.rest;
|
|
606
|
+
if (command === "upgrade") {
|
|
607
|
+
// gajaeway ops upgrade [--gjc X.Y.Z]
|
|
608
|
+
const home = gatewayHome();
|
|
609
|
+
let gjcVersion: string | undefined;
|
|
610
|
+
if (path === "--gjc" && extraArgs[0]) {
|
|
611
|
+
gjcVersion = extraArgs[0];
|
|
612
|
+
} else if (path && path !== "--gjc") {
|
|
613
|
+
throw new Error("usage: gajaeway ops upgrade [--gjc X.Y.Z]");
|
|
614
|
+
}
|
|
615
|
+
const result = await performUpgrade({ home, gjcVersion });
|
|
616
|
+
console.log(result.detail);
|
|
617
|
+
if (result.status !== "ok") {
|
|
618
|
+
process.exitCode = 1;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
// After upgrade, trigger restart-stack
|
|
622
|
+
const { receipt, supervisorPid } = await launchRestartStack({ home });
|
|
623
|
+
console.log(`restart-stack ${receipt.id}: queued (supervisor pid ${supervisorPid})`);
|
|
624
|
+
console.log("read the outcome with: gajaeway ops restart-stack --status");
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
593
627
|
if (command === "restore" && path) {
|
|
594
628
|
await restoreDatabase(parsed.socket, path);
|
|
595
629
|
break;
|
|
@@ -650,7 +684,7 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
650
684
|
process.exitCode = cycleExitCode(cycle);
|
|
651
685
|
} else
|
|
652
686
|
throw new Error(
|
|
653
|
-
"usage: gajaeway ops backup <path>|redeliver <deliveryId>|redeliver --since <iso>|cycle [--json]|integrity|restore <backupPath>|restart-stack [--status]",
|
|
687
|
+
"usage: gajaeway ops backup <path>|redeliver <deliveryId>|redeliver --since <iso>|cycle [--json]|integrity|restore <backupPath>|upgrade [--gjc X.Y.Z]|restart-stack [--status]",
|
|
654
688
|
);
|
|
655
689
|
} finally {
|
|
656
690
|
await client.close();
|
|
@@ -695,10 +729,18 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
695
729
|
console.log(JSON.stringify(await client.request<{ monitorId: string }>("monitor.update", update.params)));
|
|
696
730
|
else if (command === "list") {
|
|
697
731
|
const options = listOptions as ListOptions;
|
|
698
|
-
const result = await client.request<{
|
|
699
|
-
|
|
732
|
+
const result = await client.request<{
|
|
733
|
+
monitors: MonitorRecord[];
|
|
734
|
+
schedules: Record<string, MonitorScheduleProjection>;
|
|
735
|
+
}>("monitor.list");
|
|
736
|
+
const rows: MonitorListRow[] = result.monitors.map((monitor) => ({
|
|
737
|
+
monitor,
|
|
738
|
+
schedule: result.schedules[monitor.monitorId] ?? null,
|
|
739
|
+
}));
|
|
740
|
+
for (const line of renderList(MONITOR_COLUMNS, rows, options, {
|
|
700
741
|
key: "monitors",
|
|
701
742
|
result,
|
|
743
|
+
serializeRow: (row) => row.monitor,
|
|
702
744
|
}))
|
|
703
745
|
console.log(line);
|
|
704
746
|
} else if (command === "inspect" && args[0])
|
|
@@ -900,6 +942,24 @@ export async function main(args = process.argv.slice(2), options: MainOptions =
|
|
|
900
942
|
for (const definition of written) console.log(definition);
|
|
901
943
|
break;
|
|
902
944
|
}
|
|
945
|
+
case "migrate":
|
|
946
|
+
await migrate(parseMigrateArgs(parsed.rest));
|
|
947
|
+
break;
|
|
948
|
+
case "update": {
|
|
949
|
+
const update = parseUpdateArgs(parsed.rest);
|
|
950
|
+
const result = await runUpdate({
|
|
951
|
+
args: update,
|
|
952
|
+
home: gatewayHome(),
|
|
953
|
+
...(options.update === undefined ? {} : { deps: options.update }),
|
|
954
|
+
});
|
|
955
|
+
for (const line of renderUpdate(result)) console.log(line);
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
case "setup": {
|
|
959
|
+
// Socket-free: setup runs before any gateway exists.
|
|
960
|
+
await runSetup(parsed.rest, { home: gatewayHome(), ...options.setup });
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
903
963
|
default:
|
|
904
964
|
throw new Error(CLI_USAGE);
|
|
905
965
|
}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import type { OriginRef } from "@gajae-gateway/protocol";
|
|
3
|
+
import {
|
|
4
|
+
type ChannelPlan,
|
|
5
|
+
cronMonitor,
|
|
6
|
+
deliveryOrigin,
|
|
7
|
+
emitChannels,
|
|
8
|
+
emitImportedAxis,
|
|
9
|
+
emitMonitors,
|
|
10
|
+
emptyChannelPlan,
|
|
11
|
+
emptyExtract,
|
|
12
|
+
gatewayCron,
|
|
13
|
+
intervalCron,
|
|
14
|
+
type SourceExtract,
|
|
15
|
+
} from "./plan";
|
|
16
|
+
import {
|
|
17
|
+
entries,
|
|
18
|
+
isDirectory,
|
|
19
|
+
isRecord,
|
|
20
|
+
parseDotenv,
|
|
21
|
+
regularFile,
|
|
22
|
+
type SourceTree,
|
|
23
|
+
stringList,
|
|
24
|
+
walkFiles,
|
|
25
|
+
} from "./source-fs";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Hermes Agent (NousResearch/hermes-agent) keeps everything under one home,
|
|
29
|
+
* `~/.hermes` (HERMES_HOME):
|
|
30
|
+
*
|
|
31
|
+
* SOUL.md, AGENTS.md persona / instructions
|
|
32
|
+
* memories/MEMORY.md, USER.md bounded entry stores, entries joined by "\n§\n"
|
|
33
|
+
* .env credentials (DISCORD_BOT_TOKEN, SLACK_BOT_TOKEN, ...)
|
|
34
|
+
* config.yaml settings (discord.free_response_channels, ...)
|
|
35
|
+
* cron/jobs.json {"jobs": [...], "updated_at": ...}
|
|
36
|
+
* skills/<category>/<name>/SKILL.md, skills/.bundled_manifest
|
|
37
|
+
*
|
|
38
|
+
* See docs/migration.md for the upstream references.
|
|
39
|
+
*/
|
|
40
|
+
export const HERMES_ENTRY_DELIMITER = "\n§\n";
|
|
41
|
+
|
|
42
|
+
export async function detectHermes(root: string): Promise<boolean> {
|
|
43
|
+
return (
|
|
44
|
+
(await regularFile(join(root, "config.yaml"))) !== undefined ||
|
|
45
|
+
(await isDirectory(join(root, "memories"))) ||
|
|
46
|
+
(await regularFile(join(root, "cron", "jobs.json"))) !== undefined
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Channel tokens Hermes reads from `.env`, keyed to the gajae-way credential they become. */
|
|
51
|
+
const TOKEN_KEYS = {
|
|
52
|
+
DISCORD_BOT_TOKEN: "discord",
|
|
53
|
+
TELEGRAM_BOT_TOKEN: "telegram",
|
|
54
|
+
SLACK_BOT_TOKEN: "slack-bot",
|
|
55
|
+
SLACK_APP_TOKEN: "slack-app",
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
const ALLOWLIST_KEYS = ["DISCORD_ALLOWED_USERS", "TELEGRAM_ALLOWED_USERS", "SLACK_ALLOWED_USERS"] as const;
|
|
59
|
+
const HOME_CHANNEL_KEYS: Record<string, string> = {
|
|
60
|
+
discord: "DISCORD_HOME_CHANNEL",
|
|
61
|
+
telegram: "TELEGRAM_HOME_CHANNEL",
|
|
62
|
+
slack: "SLACK_HOME_CHANNEL",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export async function readHermes(tree: SourceTree, home: string): Promise<SourceExtract> {
|
|
66
|
+
const root = tree.root;
|
|
67
|
+
const extract = emptyExtract("hermes", root);
|
|
68
|
+
|
|
69
|
+
for (const name of ["SOUL.md", "AGENTS.md"] as const) {
|
|
70
|
+
const path = join(root, name);
|
|
71
|
+
const text = await tree.text(path);
|
|
72
|
+
if (text === undefined) continue;
|
|
73
|
+
const info = await regularFile(path);
|
|
74
|
+
extract.inventory.persona.push(tree.rel(path));
|
|
75
|
+
extract.files.push({
|
|
76
|
+
category: "persona",
|
|
77
|
+
sources: [tree.rel(path)],
|
|
78
|
+
target: `workspace/${name}`,
|
|
79
|
+
content: text,
|
|
80
|
+
mtime: info?.mtime,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const memoryDir = join(root, "memories");
|
|
85
|
+
const userPath = join(memoryDir, "USER.md");
|
|
86
|
+
const userText = await tree.text(userPath);
|
|
87
|
+
if (userText !== undefined) {
|
|
88
|
+
const info = await regularFile(userPath);
|
|
89
|
+
extract.inventory.persona.push(tree.rel(userPath));
|
|
90
|
+
extract.files.push({
|
|
91
|
+
category: "persona",
|
|
92
|
+
sources: [tree.rel(userPath)],
|
|
93
|
+
target: "workspace/USER.md",
|
|
94
|
+
content: entriesToMarkdown("User profile (imported from Hermes)", userText),
|
|
95
|
+
mtime: info?.mtime,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const memoryPath = join(memoryDir, "MEMORY.md");
|
|
99
|
+
const memoryText = await tree.text(memoryPath);
|
|
100
|
+
if (memoryText !== undefined) {
|
|
101
|
+
const info = await regularFile(memoryPath);
|
|
102
|
+
extract.inventory.memory.push(tree.rel(memoryPath));
|
|
103
|
+
extract.files.push({
|
|
104
|
+
category: "memory",
|
|
105
|
+
sources: [tree.rel(memoryPath)],
|
|
106
|
+
target: "memory/imported/hermes/MEMORY.md",
|
|
107
|
+
content: entriesToMarkdown("Long-term memory (imported from Hermes)", memoryText),
|
|
108
|
+
mtime: info?.mtime,
|
|
109
|
+
});
|
|
110
|
+
emitImportedAxis(extract);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const envPath = join(root, ".env");
|
|
114
|
+
const envText = await tree.text(envPath);
|
|
115
|
+
const env = envText === undefined ? new Map<string, string>() : parseDotenv(envText);
|
|
116
|
+
const consumedEnv = new Set<string>();
|
|
117
|
+
const configPath = join(root, "config.yaml");
|
|
118
|
+
const configText = await tree.text(configPath);
|
|
119
|
+
let config: Record<string, unknown> = {};
|
|
120
|
+
if (configText !== undefined) {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = Bun.YAML.parse(configText);
|
|
123
|
+
if (isRecord(parsed)) config = parsed;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
extract.warnings.push(
|
|
126
|
+
`config.yaml could not be parsed (${error instanceof Error ? error.message : String(error)}); channel policies were not read`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const consumedConfig = new Set<string>();
|
|
131
|
+
|
|
132
|
+
const channels = emptyChannelPlan();
|
|
133
|
+
for (const [key, credential] of Object.entries(TOKEN_KEYS)) {
|
|
134
|
+
const value = env.get(key);
|
|
135
|
+
if (!value) continue;
|
|
136
|
+
consumedEnv.add(key);
|
|
137
|
+
extract.inventory.channels.push(`.env ${key}`);
|
|
138
|
+
if (value.includes(",")) {
|
|
139
|
+
extract.unmapped.push({
|
|
140
|
+
category: "credentials",
|
|
141
|
+
source: `.env ${key}`,
|
|
142
|
+
reason:
|
|
143
|
+
"holds several comma-separated tokens (multi-workspace); gajae-way runs one bot per adapter, so none was imported",
|
|
144
|
+
});
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
channels.credentials[credential] = { value, source: `.env ${key}` };
|
|
148
|
+
}
|
|
149
|
+
for (const key of ALLOWLIST_KEYS) {
|
|
150
|
+
const value = env.get(key);
|
|
151
|
+
if (value === undefined) continue;
|
|
152
|
+
consumedEnv.add(key);
|
|
153
|
+
for (const id of stringList(value)) channels.allowlist.add(id);
|
|
154
|
+
}
|
|
155
|
+
readHermesChannelPolicy(config, env, channels, consumedConfig, consumedEnv, extract);
|
|
156
|
+
if (Object.keys(channels.credentials).length > 0 || Object.keys(channels.channels).length > 0)
|
|
157
|
+
emitChannels(extract, channels, home, ".env / config.yaml");
|
|
158
|
+
|
|
159
|
+
await readHermesCron(tree, env, consumedEnv, extract);
|
|
160
|
+
emitMonitors(extract);
|
|
161
|
+
await readHermesSkills(tree, extract);
|
|
162
|
+
|
|
163
|
+
for (const key of env.keys())
|
|
164
|
+
if (!consumedEnv.has(key))
|
|
165
|
+
extract.unmapped.push({
|
|
166
|
+
category: "credentials",
|
|
167
|
+
source: `.env ${key}`,
|
|
168
|
+
reason: /(_KEY|_TOKEN|_SECRET|PASSWORD)$/.test(key)
|
|
169
|
+
? "provider/tool credential; gajae-way sessions use the shared gjc agent profile, so set it there if still needed"
|
|
170
|
+
: "setting has no gajae-way equivalent",
|
|
171
|
+
});
|
|
172
|
+
for (const key of Object.keys(config))
|
|
173
|
+
if (!consumedConfig.has(key))
|
|
174
|
+
extract.unmapped.push({
|
|
175
|
+
category: "other",
|
|
176
|
+
source: `config.yaml ${key}`,
|
|
177
|
+
reason: "setting has no gajae-way equivalent",
|
|
178
|
+
});
|
|
179
|
+
return extract;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Hermes stores memory as delimited entries; gajae-way memory is Markdown, one bullet block per entry. */
|
|
183
|
+
export function entriesToMarkdown(title: string, raw: string): string {
|
|
184
|
+
const items = raw
|
|
185
|
+
.split(HERMES_ENTRY_DELIMITER)
|
|
186
|
+
.map((entry) => entry.trim())
|
|
187
|
+
.filter(Boolean);
|
|
188
|
+
return `# ${title}\n\n${items.map((entry) => `- ${entry.replace(/\n/g, "\n ")}`).join("\n")}\n`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readHermesChannelPolicy(
|
|
192
|
+
config: Record<string, unknown>,
|
|
193
|
+
env: Map<string, string>,
|
|
194
|
+
channels: ChannelPlan,
|
|
195
|
+
consumedConfig: Set<string>,
|
|
196
|
+
consumedEnv: Set<string>,
|
|
197
|
+
extract: SourceExtract,
|
|
198
|
+
): void {
|
|
199
|
+
const discord = isRecord(config.discord) ? config.discord : undefined;
|
|
200
|
+
if (discord) {
|
|
201
|
+
consumedConfig.add("discord");
|
|
202
|
+
const free = stringList(discord.free_response_channels);
|
|
203
|
+
for (const id of free) {
|
|
204
|
+
if (id === "*") {
|
|
205
|
+
extract.unmapped.push({
|
|
206
|
+
category: "channels",
|
|
207
|
+
source: "config.yaml discord.free_response_channels",
|
|
208
|
+
reason: '"*" (every channel) has no per-channel equivalent; list channels explicitly',
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
channels.channels[`discord:${id}`] = { engagement: "open" };
|
|
213
|
+
extract.inventory.channels.push(`config.yaml discord.free_response_channels ${id}`);
|
|
214
|
+
}
|
|
215
|
+
for (const key of Object.keys(discord))
|
|
216
|
+
if (key !== "free_response_channels" && key !== "require_mention")
|
|
217
|
+
extract.unmapped.push({
|
|
218
|
+
category: "channels",
|
|
219
|
+
source: `config.yaml discord.${key}`,
|
|
220
|
+
reason: "Discord adapter setting has no gajae-way equivalent",
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
const slackChannels = env.get("SLACK_ALLOWED_CHANNELS");
|
|
224
|
+
if (slackChannels !== undefined) {
|
|
225
|
+
consumedEnv.add("SLACK_ALLOWED_CHANNELS");
|
|
226
|
+
for (const id of stringList(slackChannels)) channels.channels[`slack:${id}`] = { engagement: "mention-open" };
|
|
227
|
+
}
|
|
228
|
+
const telegram = isRecord(config.telegram) ? config.telegram : undefined;
|
|
229
|
+
if (telegram) consumedConfig.add("telegram");
|
|
230
|
+
const telegramGroups = env.get("TELEGRAM_GROUP_ALLOWED_CHATS");
|
|
231
|
+
if (telegramGroups !== undefined) {
|
|
232
|
+
consumedEnv.add("TELEGRAM_GROUP_ALLOWED_CHATS");
|
|
233
|
+
const engagement = telegram?.require_mention === true ? "mention-open" : "open";
|
|
234
|
+
for (const id of stringList(telegramGroups)) channels.channels[`telegram:${id}`] = { engagement };
|
|
235
|
+
}
|
|
236
|
+
for (const key of [
|
|
237
|
+
"DISCORD_ALLOW_ALL_USERS",
|
|
238
|
+
"TELEGRAM_ALLOW_ALL_USERS",
|
|
239
|
+
"SLACK_ALLOW_ALL_USERS",
|
|
240
|
+
"GATEWAY_ALLOW_ALL_USERS",
|
|
241
|
+
]) {
|
|
242
|
+
if (!env.has(key)) continue;
|
|
243
|
+
consumedEnv.add(key);
|
|
244
|
+
extract.unmapped.push({
|
|
245
|
+
category: "channels",
|
|
246
|
+
source: `.env ${key}`,
|
|
247
|
+
reason:
|
|
248
|
+
'allow-everyone is not carried over; gajae-way DMs default to the allowlist. Set dmPolicy "open" in config.json deliberately if wanted',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function readHermesCron(
|
|
254
|
+
tree: SourceTree,
|
|
255
|
+
env: Map<string, string>,
|
|
256
|
+
consumedEnv: Set<string>,
|
|
257
|
+
extract: SourceExtract,
|
|
258
|
+
): Promise<void> {
|
|
259
|
+
const path = join(tree.root, "cron", "jobs.json");
|
|
260
|
+
const text = await tree.text(path);
|
|
261
|
+
if (text === undefined) return;
|
|
262
|
+
let jobs: unknown[] = [];
|
|
263
|
+
try {
|
|
264
|
+
const parsed: unknown = JSON.parse(text);
|
|
265
|
+
jobs = Array.isArray(parsed) ? parsed : isRecord(parsed) && Array.isArray(parsed.jobs) ? parsed.jobs : [];
|
|
266
|
+
} catch {
|
|
267
|
+
extract.unmapped.push({ category: "monitors", source: tree.rel(path), reason: "is not valid JSON" });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const taken = new Set<string>();
|
|
271
|
+
for (const job of jobs) {
|
|
272
|
+
if (!isRecord(job)) continue;
|
|
273
|
+
const label = String(job.name ?? job.id ?? "job");
|
|
274
|
+
const source = `${tree.rel(path)} ${JSON.stringify(label)}`;
|
|
275
|
+
extract.inventory.schedules.push(source);
|
|
276
|
+
const skip = (reason: string) => extract.unmapped.push({ category: "monitors", source, reason });
|
|
277
|
+
if (job.no_agent === true || (typeof job.script === "string" && job.script)) {
|
|
278
|
+
skip("script job: gajae-way script monitors run a command under scriptRoot, recreate it with a script trigger");
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (typeof job.monitor_script === "string" || typeof job.monitor_url === "string") {
|
|
282
|
+
skip("change-detection pre-check (monitor_script/monitor_url) has no equivalent");
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const schedule = isRecord(job.schedule) ? job.schedule : {};
|
|
286
|
+
let cron: string | undefined;
|
|
287
|
+
if (schedule.kind === "cron" && typeof schedule.expr === "string") {
|
|
288
|
+
cron = gatewayCron(schedule.expr);
|
|
289
|
+
if (!cron) {
|
|
290
|
+
skip(`cron expression ${JSON.stringify(schedule.expr)} uses syntax the gateway cron trigger does not evaluate`);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
} else if (schedule.kind === "interval" && typeof schedule.minutes === "number") {
|
|
294
|
+
cron = intervalCron(schedule.minutes);
|
|
295
|
+
if (!cron) {
|
|
296
|
+
skip(`every ${schedule.minutes}m does not divide an hour or a day, so it has no cron form`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
skip(`schedule kind ${JSON.stringify(schedule.kind)} (one-shot) has no recurring monitor equivalent`);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const skills = stringList(job.skills);
|
|
304
|
+
const prompt = typeof job.prompt === "string" ? job.prompt : "";
|
|
305
|
+
const instruction = skills.length ? `Use the skill(s): ${skills.join(", ")}.\n\n${prompt}` : prompt;
|
|
306
|
+
const target = hermesTarget(job, env, consumedEnv, extract, source);
|
|
307
|
+
const repeat = isRecord(job.repeat) ? job.repeat.times : null;
|
|
308
|
+
if (typeof repeat === "number")
|
|
309
|
+
extract.warnings.push(`${source}: repeat limit ${repeat} dropped; the monitor fires until removed`);
|
|
310
|
+
const spec = cronMonitor({
|
|
311
|
+
source: "hermes",
|
|
312
|
+
name: label,
|
|
313
|
+
schedule: cron,
|
|
314
|
+
instruction,
|
|
315
|
+
enabled: job.enabled !== false && job.state !== "paused",
|
|
316
|
+
target,
|
|
317
|
+
taken,
|
|
318
|
+
});
|
|
319
|
+
if (typeof spec === "string") skip(spec);
|
|
320
|
+
else extract.monitors.push({ source, spec });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function hermesTarget(
|
|
325
|
+
job: Record<string, unknown>,
|
|
326
|
+
env: Map<string, string>,
|
|
327
|
+
consumedEnv: Set<string>,
|
|
328
|
+
extract: SourceExtract,
|
|
329
|
+
source: string,
|
|
330
|
+
): OriginRef | undefined {
|
|
331
|
+
const targets = stringList(job.deliver ?? "local").filter((target) => target !== "local");
|
|
332
|
+
if (targets.length === 0) return undefined;
|
|
333
|
+
const resolved: OriginRef[] = [];
|
|
334
|
+
for (const target of targets) {
|
|
335
|
+
let result: OriginRef | string;
|
|
336
|
+
if (target === "origin" || target === "origin_fallback") {
|
|
337
|
+
const origin = isRecord(job.origin) ? job.origin : undefined;
|
|
338
|
+
result =
|
|
339
|
+
origin && typeof origin.platform === "string" && origin.chat_id !== undefined
|
|
340
|
+
? deliveryOrigin(origin.platform, String(origin.chat_id))
|
|
341
|
+
: "origin delivery recorded no origin chat";
|
|
342
|
+
} else {
|
|
343
|
+
const [platform = "", ...rest] = target.split(":");
|
|
344
|
+
let chat = rest.join(":");
|
|
345
|
+
if (!chat && HOME_CHANNEL_KEYS[platform]) {
|
|
346
|
+
chat = env.get(HOME_CHANNEL_KEYS[platform] as string) ?? "";
|
|
347
|
+
consumedEnv.add(HOME_CHANNEL_KEYS[platform] as string);
|
|
348
|
+
consumedEnv.add(`${HOME_CHANNEL_KEYS[platform]}_NAME`);
|
|
349
|
+
}
|
|
350
|
+
result = chat
|
|
351
|
+
? deliveryOrigin(platform, chat)
|
|
352
|
+
: `delivery target ${JSON.stringify(target)} has no chat id or home channel`;
|
|
353
|
+
}
|
|
354
|
+
if (typeof result === "string")
|
|
355
|
+
extract.warnings.push(`${source}: ${result}; delivered to the owner target instead`);
|
|
356
|
+
else resolved.push(result);
|
|
357
|
+
}
|
|
358
|
+
if (resolved.length > 1)
|
|
359
|
+
extract.warnings.push(`${source}: a monitor has one channel target; kept the first of ${resolved.length}`);
|
|
360
|
+
return resolved[0];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function readHermesSkills(tree: SourceTree, extract: SourceExtract): Promise<void> {
|
|
364
|
+
const skillsDir = join(tree.root, "skills");
|
|
365
|
+
if (!(await isDirectory(skillsDir))) return;
|
|
366
|
+
tree.consume(skillsDir);
|
|
367
|
+
const bundledText = await tree.text(join(skillsDir, ".bundled_manifest"));
|
|
368
|
+
const bundled = new Set(
|
|
369
|
+
(bundledText ?? "")
|
|
370
|
+
.split(/\r?\n/)
|
|
371
|
+
.map((line) => line.split(":")[0]?.trim() ?? "")
|
|
372
|
+
.filter(Boolean),
|
|
373
|
+
);
|
|
374
|
+
const files = (await walkFiles(skillsDir)).filter((file) => !file.split("/").some((part) => part.startsWith(".")));
|
|
375
|
+
const skillRoots = files
|
|
376
|
+
.filter((file) => file.endsWith("/SKILL.md") || file === "SKILL.md")
|
|
377
|
+
.map((file) => file.slice(0, -"/SKILL.md".length));
|
|
378
|
+
const seen = new Set<string>();
|
|
379
|
+
for (const skillRoot of skillRoots) {
|
|
380
|
+
const name = skillRoot.split("/").at(-1) ?? skillRoot;
|
|
381
|
+
if (bundled.has(name)) continue;
|
|
382
|
+
await copySkill(tree, join(skillsDir, skillRoot), name, extract, seen);
|
|
383
|
+
}
|
|
384
|
+
if (bundled.size)
|
|
385
|
+
extract.warnings.push(`${bundled.size} Hermes-bundled skill(s) skipped; they ship with Hermes, not with your data`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export async function copySkill(
|
|
389
|
+
tree: SourceTree,
|
|
390
|
+
dir: string,
|
|
391
|
+
name: string,
|
|
392
|
+
extract: SourceExtract,
|
|
393
|
+
seen: Set<string>,
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
const source = tree.rel(dir);
|
|
396
|
+
extract.inventory.skills.push(source);
|
|
397
|
+
if (name === "self-ops" || seen.has(name)) {
|
|
398
|
+
extract.unmapped.push({
|
|
399
|
+
category: "skills",
|
|
400
|
+
source,
|
|
401
|
+
reason: `a skill named ${name} is already imported or built in`,
|
|
402
|
+
});
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
seen.add(name);
|
|
406
|
+
for (const file of await walkFiles(dir)) {
|
|
407
|
+
const path = join(dir, file);
|
|
408
|
+
const content = await tree.bytes(path);
|
|
409
|
+
if (content === undefined) continue;
|
|
410
|
+
extract.files.push({
|
|
411
|
+
category: "skills",
|
|
412
|
+
sources: [tree.rel(path)],
|
|
413
|
+
target: `workspace/.gjc/skills/${name}/${file}`,
|
|
414
|
+
content,
|
|
415
|
+
mtime: (await regularFile(path))?.mtime,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Top-level entries no reader consumed, reported so nothing is dropped silently. */
|
|
421
|
+
export async function reportLeftovers(
|
|
422
|
+
tree: SourceTree,
|
|
423
|
+
dir: string,
|
|
424
|
+
extract: SourceExtract,
|
|
425
|
+
reason: (name: string) => string,
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
for (const name of await entries(dir)) {
|
|
428
|
+
const path = join(dir, name);
|
|
429
|
+
if (tree.isFullyConsumed(path)) continue;
|
|
430
|
+
if (tree.isConsumed(path)) {
|
|
431
|
+
if (await isDirectory(path)) await reportLeftovers(tree, path, extract, reason);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
extract.unmapped.push({ category: "other", source: tree.rel(path), reason: reason(name) });
|
|
435
|
+
}
|
|
436
|
+
}
|