@ectplsm/relic 0.1.1 → 0.1.3
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/README.md +172 -49
- package/dist/adapters/shells/claude-hook.d.ts +12 -0
- package/dist/adapters/shells/claude-hook.js +160 -0
- package/dist/adapters/shells/claude-shell.d.ts +5 -2
- package/dist/adapters/shells/claude-shell.js +17 -3
- package/dist/adapters/shells/codex-hook.d.ts +12 -0
- package/dist/adapters/shells/codex-hook.js +141 -0
- package/dist/adapters/shells/codex-shell.d.ts +7 -4
- package/dist/adapters/shells/codex-shell.js +22 -7
- package/dist/adapters/shells/copilot-shell.d.ts +2 -2
- package/dist/adapters/shells/copilot-shell.js +3 -3
- package/dist/adapters/shells/gemini-hook.d.ts +12 -0
- package/dist/adapters/shells/gemini-hook.js +108 -0
- package/dist/adapters/shells/gemini-shell.d.ts +6 -4
- package/dist/adapters/shells/gemini-shell.js +103 -14
- package/dist/adapters/shells/index.d.ts +0 -1
- package/dist/adapters/shells/index.js +0 -1
- package/dist/adapters/shells/spawn-shell.d.ts +1 -1
- package/dist/adapters/shells/spawn-shell.js +10 -3
- package/dist/adapters/shells/trust-registrar.d.ts +19 -0
- package/dist/adapters/shells/trust-registrar.js +141 -0
- package/dist/core/ports/shell-launcher.d.ts +17 -2
- package/dist/core/usecases/archive-cursor-update.d.ts +21 -0
- package/dist/core/usecases/archive-cursor-update.js +44 -0
- package/dist/core/usecases/archive-pending.d.ts +25 -0
- package/dist/core/usecases/archive-pending.js +61 -0
- package/dist/core/usecases/archive-search.d.ts +20 -0
- package/dist/core/usecases/archive-search.js +46 -0
- package/dist/core/usecases/inbox-search.d.ts +20 -0
- package/dist/core/usecases/inbox-search.js +46 -0
- package/dist/core/usecases/inbox-write.d.ts +27 -0
- package/dist/core/usecases/inbox-write.js +72 -0
- package/dist/core/usecases/index.d.ts +2 -1
- package/dist/core/usecases/index.js +2 -1
- package/dist/core/usecases/summon.d.ts +6 -1
- package/dist/core/usecases/summon.js +8 -2
- package/dist/interfaces/cli/commands/config.d.ts +2 -0
- package/dist/interfaces/cli/commands/config.js +83 -0
- package/dist/interfaces/cli/commands/extract.js +3 -2
- package/dist/interfaces/cli/commands/init.js +47 -0
- package/dist/interfaces/cli/commands/inject.js +3 -2
- package/dist/interfaces/cli/commands/shell.js +13 -11
- package/dist/interfaces/cli/index.js +8 -1
- package/dist/interfaces/mcp/index.js +68 -305
- package/dist/shared/config.d.ts +31 -0
- package/dist/shared/config.js +86 -16
- package/dist/shared/engram-composer.d.ts +16 -3
- package/dist/shared/engram-composer.js +50 -5
- package/dist/shared/memory-inbox.d.ts +78 -0
- package/dist/shared/memory-inbox.js +168 -0
- package/dist/shared/session-recorder.d.ts +47 -0
- package/dist/shared/session-recorder.js +112 -0
- package/package.json +5 -5
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const RELIC_DIR = join(homedir(), ".relic");
|
|
5
|
+
const HOOKS_DIR = join(RELIC_DIR, "hooks");
|
|
6
|
+
export const CODEX_HOOK_SCRIPT_PATH = join(HOOKS_DIR, "codex-stop.js");
|
|
7
|
+
const CODEX_HOOKS_PATH = join(homedir(), ".codex", "hooks.json");
|
|
8
|
+
const RELIC_HOOK_COMMAND = `node ${join(HOOKS_DIR, "codex-stop.js")}`;
|
|
9
|
+
/**
|
|
10
|
+
* Stop hook スクリプトの内容。
|
|
11
|
+
* Codex CLI の各ターン終了後に発火し、会話ログを Engram archive に追記する。
|
|
12
|
+
* RELIC_ENGRAM_ID 環境変数で対象 Engram ID を受け取る。
|
|
13
|
+
* stdin には { last_assistant_message, transcript_path, session_id, ... } が渡される。
|
|
14
|
+
* Claude の Stop hook と異なり last_assistant_message が直接取得できるため wait 不要。
|
|
15
|
+
*/
|
|
16
|
+
const HOOK_SCRIPT = `#!/usr/bin/env node
|
|
17
|
+
// Relic Stop hook for Codex CLI
|
|
18
|
+
// Automatically logs each conversation turn to the Engram archive.
|
|
19
|
+
// Receives Stop hook JSON on stdin.
|
|
20
|
+
const { appendFileSync, existsSync, readFileSync } = require("node:fs");
|
|
21
|
+
const { join } = require("node:path");
|
|
22
|
+
const { homedir } = require("node:os");
|
|
23
|
+
|
|
24
|
+
let raw = "";
|
|
25
|
+
process.stdin.setEncoding("utf-8");
|
|
26
|
+
process.stdin.on("data", (chunk) => { raw += chunk; });
|
|
27
|
+
process.stdin.on("end", () => {
|
|
28
|
+
try {
|
|
29
|
+
const input = JSON.parse(raw);
|
|
30
|
+
const engramId = process.env.RELIC_ENGRAM_ID;
|
|
31
|
+
if (!engramId) process.exit(0);
|
|
32
|
+
|
|
33
|
+
const archivePath = join(homedir(), ".relic", "engrams", engramId, "archive.md");
|
|
34
|
+
if (!existsSync(archivePath)) process.exit(0);
|
|
35
|
+
|
|
36
|
+
// Codex Stop hook は last_assistant_message を直接提供する
|
|
37
|
+
const lastResponse = (input.last_assistant_message || "").trim();
|
|
38
|
+
|
|
39
|
+
// transcript から最後のユーザー入力を取得
|
|
40
|
+
// Codex transcript format:
|
|
41
|
+
// { type: "response_item", payload: { type: "message", role: "user", content: [{ type: "input_text", text: "..." }] } }
|
|
42
|
+
// <environment_context> で始まるエントリはシステム注入なのでスキップする
|
|
43
|
+
let lastPrompt = "";
|
|
44
|
+
const transcriptPath = input.transcript_path;
|
|
45
|
+
if (transcriptPath && existsSync(transcriptPath)) {
|
|
46
|
+
const lines = readFileSync(transcriptPath, "utf-8")
|
|
47
|
+
.split("\\n")
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
|
|
52
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
53
|
+
const entry = lines[i];
|
|
54
|
+
if (entry.type !== "response_item") continue;
|
|
55
|
+
const p = entry.payload;
|
|
56
|
+
if (!p || p.role !== "user" || p.type !== "message") continue;
|
|
57
|
+
const content = p.content;
|
|
58
|
+
if (Array.isArray(content)) {
|
|
59
|
+
const texts = content
|
|
60
|
+
.filter((c) => c.type === "input_text" && c.text && !c.text.trimStart().startsWith("<environment_context>"))
|
|
61
|
+
.map((c) => c.text.trim());
|
|
62
|
+
if (texts.length > 0) {
|
|
63
|
+
lastPrompt = texts.join("\\n").trim();
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!lastPrompt && !lastResponse) process.exit(0);
|
|
71
|
+
|
|
72
|
+
const date = new Date().toISOString().split("T")[0];
|
|
73
|
+
const summary = lastPrompt.slice(0, 80).replace(/\\n/g, " ");
|
|
74
|
+
const entry = \`\\n---\\n\${date} | \${summary}\\n\${lastResponse}\\n\`;
|
|
75
|
+
appendFileSync(archivePath, entry, "utf-8");
|
|
76
|
+
} catch {
|
|
77
|
+
// silently ignore
|
|
78
|
+
}
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|
|
81
|
+
`;
|
|
82
|
+
/**
|
|
83
|
+
* Codex CLI の Stop フックをセットアップする。
|
|
84
|
+
* - ~/.relic/hooks/codex-stop.js を生成
|
|
85
|
+
* - ~/.codex/hooks.json に Stop フックを登録
|
|
86
|
+
* 既にセットアップ済みの場合はスキップ。
|
|
87
|
+
*/
|
|
88
|
+
export function setupCodexHook() {
|
|
89
|
+
// 1. フックスクリプトを生成
|
|
90
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
91
|
+
writeFileSync(CODEX_HOOK_SCRIPT_PATH, HOOK_SCRIPT, { encoding: "utf-8", mode: 0o755 });
|
|
92
|
+
// 2. ~/.codex/hooks.json に Stop フックを登録
|
|
93
|
+
const codexDir = join(homedir(), ".codex");
|
|
94
|
+
mkdirSync(codexDir, { recursive: true });
|
|
95
|
+
let hooksConfig = {};
|
|
96
|
+
if (existsSync(CODEX_HOOKS_PATH)) {
|
|
97
|
+
try {
|
|
98
|
+
hooksConfig = JSON.parse(readFileSync(CODEX_HOOKS_PATH, "utf-8"));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
hooksConfig = {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const hooks = (hooksConfig.hooks ?? {});
|
|
105
|
+
const stopHooks = (hooks.Stop ?? []);
|
|
106
|
+
// 既に登録済みならスキップ
|
|
107
|
+
const alreadyRegistered = stopHooks.some((group) => group.hooks?.some((h) => h.command === RELIC_HOOK_COMMAND));
|
|
108
|
+
if (alreadyRegistered)
|
|
109
|
+
return;
|
|
110
|
+
hooks.Stop = [
|
|
111
|
+
...stopHooks,
|
|
112
|
+
{
|
|
113
|
+
hooks: [
|
|
114
|
+
{
|
|
115
|
+
type: "command",
|
|
116
|
+
command: RELIC_HOOK_COMMAND,
|
|
117
|
+
timeout: 5,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
hooksConfig.hooks = hooks;
|
|
123
|
+
writeFileSync(CODEX_HOOKS_PATH, JSON.stringify(hooksConfig, null, 2), "utf-8");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Stop フックがセットアップ済みか確認する。
|
|
127
|
+
*/
|
|
128
|
+
export function isCodexHookSetup() {
|
|
129
|
+
if (!existsSync(CODEX_HOOK_SCRIPT_PATH))
|
|
130
|
+
return false;
|
|
131
|
+
if (!existsSync(CODEX_HOOKS_PATH))
|
|
132
|
+
return false;
|
|
133
|
+
try {
|
|
134
|
+
const hooksConfig = JSON.parse(readFileSync(CODEX_HOOKS_PATH, "utf-8"));
|
|
135
|
+
const stopHooks = hooksConfig.hooks?.Stop ?? [];
|
|
136
|
+
return stopHooks.some((group) => group.hooks?.some((h) => h.command === RELIC_HOOK_COMMAND));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { ShellLauncher, InjectionMode } from "../../core/ports/shell-launcher.js";
|
|
1
|
+
import type { ShellLauncher, InjectionMode, ShellLaunchOptions } from "../../core/ports/shell-launcher.js";
|
|
2
2
|
/**
|
|
3
3
|
* Codex CLI アダプター
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* `-c developer_instructions=<prompt>` でEngramをdeveloperロールとして注入する。
|
|
5
|
+
* user-messageよりシステムプロンプトに近い強度で注入できる。
|
|
6
|
+
*
|
|
7
|
+
* 初回起動時に Stop フックを ~/.codex/hooks.json に登録し、
|
|
8
|
+
* 各ターン終了後に会話ログを Engram archive に自動記録する。
|
|
6
9
|
*/
|
|
7
10
|
export declare class CodexShell implements ShellLauncher {
|
|
8
11
|
private readonly command;
|
|
@@ -10,5 +13,5 @@ export declare class CodexShell implements ShellLauncher {
|
|
|
10
13
|
readonly injectionMode: InjectionMode;
|
|
11
14
|
constructor(command?: string);
|
|
12
15
|
isAvailable(): Promise<boolean>;
|
|
13
|
-
launch(prompt: string,
|
|
16
|
+
launch(prompt: string, options?: ShellLaunchOptions): Promise<void>;
|
|
14
17
|
}
|
|
@@ -2,16 +2,20 @@ import { exec } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { spawnShell } from "./spawn-shell.js";
|
|
4
4
|
import { wrapWithOverride } from "./override-preamble.js";
|
|
5
|
+
import { setupCodexHook, isCodexHookSetup } from "./codex-hook.js";
|
|
5
6
|
const execAsync = promisify(exec);
|
|
6
7
|
/**
|
|
7
8
|
* Codex CLI アダプター
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* `-c developer_instructions=<prompt>` でEngramをdeveloperロールとして注入する。
|
|
10
|
+
* user-messageよりシステムプロンプトに近い強度で注入できる。
|
|
11
|
+
*
|
|
12
|
+
* 初回起動時に Stop フックを ~/.codex/hooks.json に登録し、
|
|
13
|
+
* 各ターン終了後に会話ログを Engram archive に自動記録する。
|
|
10
14
|
*/
|
|
11
15
|
export class CodexShell {
|
|
12
16
|
command;
|
|
13
17
|
name = "Codex CLI";
|
|
14
|
-
injectionMode = "
|
|
18
|
+
injectionMode = "developer-message";
|
|
15
19
|
constructor(command = "codex") {
|
|
16
20
|
this.command = command;
|
|
17
21
|
}
|
|
@@ -24,11 +28,22 @@ export class CodexShell {
|
|
|
24
28
|
return false;
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
|
-
async launch(prompt,
|
|
31
|
+
async launch(prompt, options) {
|
|
32
|
+
// Stop フックを初回のみセットアップ
|
|
33
|
+
if (!isCodexHookSetup()) {
|
|
34
|
+
console.log("Setting up Codex CLI Stop hook (first run only)...");
|
|
35
|
+
setupCodexHook();
|
|
36
|
+
console.log("Hook registered to ~/.codex/hooks.json");
|
|
37
|
+
console.log();
|
|
38
|
+
}
|
|
28
39
|
const args = [
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
"-c", `developer_instructions=${JSON.stringify(wrapWithOverride(prompt))}`,
|
|
41
|
+
"-c", "features.codex_hooks=true",
|
|
42
|
+
...(options?.extraArgs ?? []),
|
|
31
43
|
];
|
|
32
|
-
|
|
44
|
+
const env = {};
|
|
45
|
+
if (options?.engramId)
|
|
46
|
+
env.RELIC_ENGRAM_ID = options.engramId;
|
|
47
|
+
await spawnShell(this.command, args, options?.cwd, Object.keys(env).length > 0 ? env : undefined);
|
|
33
48
|
}
|
|
34
49
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ShellLauncher, InjectionMode } from "../../core/ports/shell-launcher.js";
|
|
1
|
+
import type { ShellLauncher, InjectionMode, ShellLaunchOptions } from "../../core/ports/shell-launcher.js";
|
|
2
2
|
/**
|
|
3
3
|
* GitHub Copilot CLI アダプター (copilot コマンド)
|
|
4
4
|
* --interactive フラグでEngramを初回プロンプトとして注入し、
|
|
@@ -10,5 +10,5 @@ export declare class CopilotShell implements ShellLauncher {
|
|
|
10
10
|
readonly injectionMode: InjectionMode;
|
|
11
11
|
constructor(command?: string);
|
|
12
12
|
isAvailable(): Promise<boolean>;
|
|
13
|
-
launch(prompt: string,
|
|
13
|
+
launch(prompt: string, options?: ShellLaunchOptions): Promise<void>;
|
|
14
14
|
}
|
|
@@ -25,16 +25,16 @@ export class CopilotShell {
|
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
async launch(prompt,
|
|
28
|
+
async launch(prompt, options) {
|
|
29
29
|
const tmp = writeTempPrompt(wrapWithOverride(prompt));
|
|
30
30
|
try {
|
|
31
31
|
const fileContent = readFileSync(tmp.path, "utf-8");
|
|
32
32
|
const args = [
|
|
33
33
|
"--interactive",
|
|
34
34
|
fileContent,
|
|
35
|
-
...extraArgs,
|
|
35
|
+
...(options?.extraArgs ?? []),
|
|
36
36
|
];
|
|
37
|
-
await spawnShell(this.command, args, cwd);
|
|
37
|
+
await spawnShell(this.command, args, options?.cwd);
|
|
38
38
|
}
|
|
39
39
|
finally {
|
|
40
40
|
tmp.cleanup();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const GEMINI_HOOK_SCRIPT_PATH: string;
|
|
2
|
+
/**
|
|
3
|
+
* Gemini CLI の AfterAgent フックをセットアップする。
|
|
4
|
+
* - ~/.relic/hooks/gemini-after-agent.js を生成
|
|
5
|
+
* - ~/.gemini/settings.json に AfterAgent フックを登録
|
|
6
|
+
* 既にセットアップ済みの場合はスキップ。
|
|
7
|
+
*/
|
|
8
|
+
export declare function setupGeminiHook(): void;
|
|
9
|
+
/**
|
|
10
|
+
* AfterAgent フックがセットアップ済みか確認する。
|
|
11
|
+
*/
|
|
12
|
+
export declare function isGeminiHookSetup(): boolean;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const RELIC_DIR = join(homedir(), ".relic");
|
|
5
|
+
const HOOKS_DIR = join(RELIC_DIR, "hooks");
|
|
6
|
+
export const GEMINI_HOOK_SCRIPT_PATH = join(HOOKS_DIR, "gemini-after-agent.js");
|
|
7
|
+
const GEMINI_SETTINGS_PATH = join(homedir(), ".gemini", "settings.json");
|
|
8
|
+
const RELIC_HOOK_NAME = "relic-archive-log";
|
|
9
|
+
/**
|
|
10
|
+
* AfterAgent hook スクリプトの内容。
|
|
11
|
+
* Gemini CLI の各ターン終了後に発火し、会話ログを Engram archive に追記する。
|
|
12
|
+
* RELIC_ENGRAM_ID 環境変数で対象 Engram ID を受け取る。
|
|
13
|
+
*/
|
|
14
|
+
const HOOK_SCRIPT = `#!/usr/bin/env node
|
|
15
|
+
// Relic AfterAgent hook for Gemini CLI
|
|
16
|
+
// Automatically logs each conversation turn to the Engram archive.
|
|
17
|
+
// Receives AfterAgentInput JSON on stdin.
|
|
18
|
+
const { appendFileSync, existsSync } = require("node:fs");
|
|
19
|
+
const { join } = require("node:path");
|
|
20
|
+
const { homedir } = require("node:os");
|
|
21
|
+
|
|
22
|
+
let raw = "";
|
|
23
|
+
process.stdin.setEncoding("utf-8");
|
|
24
|
+
process.stdin.on("data", (chunk) => { raw += chunk; });
|
|
25
|
+
process.stdin.on("end", () => {
|
|
26
|
+
try {
|
|
27
|
+
const input = JSON.parse(raw);
|
|
28
|
+
const engramId = process.env.RELIC_ENGRAM_ID;
|
|
29
|
+
if (!engramId) process.exit(0);
|
|
30
|
+
|
|
31
|
+
const prompt = (input.prompt || "").trim();
|
|
32
|
+
const response = (input.prompt_response || "").trim();
|
|
33
|
+
if (!prompt && !response) process.exit(0);
|
|
34
|
+
|
|
35
|
+
const archivePath = join(homedir(), ".relic", "engrams", engramId, "archive.md");
|
|
36
|
+
if (!existsSync(archivePath)) process.exit(0);
|
|
37
|
+
|
|
38
|
+
const date = new Date().toISOString().split("T")[0];
|
|
39
|
+
const summary = prompt.slice(0, 80).replace(/\\n/g, " ");
|
|
40
|
+
const entry = \`\\n---\\n\${date} | \${summary}\\n\${response}\\n\`;
|
|
41
|
+
appendFileSync(archivePath, entry, "utf-8");
|
|
42
|
+
} catch {
|
|
43
|
+
// silently ignore
|
|
44
|
+
}
|
|
45
|
+
process.exit(0);
|
|
46
|
+
});
|
|
47
|
+
`;
|
|
48
|
+
/**
|
|
49
|
+
* Gemini CLI の AfterAgent フックをセットアップする。
|
|
50
|
+
* - ~/.relic/hooks/gemini-after-agent.js を生成
|
|
51
|
+
* - ~/.gemini/settings.json に AfterAgent フックを登録
|
|
52
|
+
* 既にセットアップ済みの場合はスキップ。
|
|
53
|
+
*/
|
|
54
|
+
export function setupGeminiHook() {
|
|
55
|
+
// 1. フックスクリプトを生成
|
|
56
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
57
|
+
writeFileSync(GEMINI_HOOK_SCRIPT_PATH, HOOK_SCRIPT, { encoding: "utf-8", mode: 0o755 });
|
|
58
|
+
// 2. ~/.gemini/settings.json にフックを登録
|
|
59
|
+
const geminiDir = join(homedir(), ".gemini");
|
|
60
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
61
|
+
let settings = {};
|
|
62
|
+
if (existsSync(GEMINI_SETTINGS_PATH)) {
|
|
63
|
+
try {
|
|
64
|
+
settings = JSON.parse(readFileSync(GEMINI_SETTINGS_PATH, "utf-8"));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
settings = {};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const hooks = (settings.hooks ?? {});
|
|
71
|
+
const afterAgentHooks = (hooks.AfterAgent ?? []);
|
|
72
|
+
// 既に登録済みならスキップ
|
|
73
|
+
const alreadyRegistered = afterAgentHooks.some((group) => group.hooks?.some((h) => h.name === RELIC_HOOK_NAME));
|
|
74
|
+
if (alreadyRegistered)
|
|
75
|
+
return;
|
|
76
|
+
hooks.AfterAgent = [
|
|
77
|
+
...afterAgentHooks,
|
|
78
|
+
{
|
|
79
|
+
hooks: [
|
|
80
|
+
{
|
|
81
|
+
type: "command",
|
|
82
|
+
command: `node ${GEMINI_HOOK_SCRIPT_PATH}`,
|
|
83
|
+
name: RELIC_HOOK_NAME,
|
|
84
|
+
timeout: 5000,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
settings.hooks = hooks;
|
|
90
|
+
writeFileSync(GEMINI_SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf-8");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* AfterAgent フックがセットアップ済みか確認する。
|
|
94
|
+
*/
|
|
95
|
+
export function isGeminiHookSetup() {
|
|
96
|
+
if (!existsSync(GEMINI_HOOK_SCRIPT_PATH))
|
|
97
|
+
return false;
|
|
98
|
+
if (!existsSync(GEMINI_SETTINGS_PATH))
|
|
99
|
+
return false;
|
|
100
|
+
try {
|
|
101
|
+
const settings = JSON.parse(readFileSync(GEMINI_SETTINGS_PATH, "utf-8"));
|
|
102
|
+
const afterAgentHooks = settings.hooks?.AfterAgent ?? [];
|
|
103
|
+
return afterAgentHooks.some((group) => group.hooks?.some((h) => h.name === RELIC_HOOK_NAME));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import type { ShellLauncher, InjectionMode } from "../../core/ports/shell-launcher.js";
|
|
1
|
+
import type { ShellLauncher, InjectionMode, ShellLaunchOptions } from "../../core/ports/shell-launcher.js";
|
|
2
2
|
/**
|
|
3
3
|
* Gemini CLI アダプター
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* GEMINI_SYSTEM_MD 環境変数でシステムプロンプトを完全上書きし、
|
|
5
|
+
* Engramを組み込みプロンプトと合成して注入する。
|
|
6
|
+
*
|
|
7
|
+
* 初回起動時のみ GEMINI_WRITE_SYSTEM_MD でデフォルトプロンプトをキャプチャ・キャッシュする。
|
|
6
8
|
*/
|
|
7
9
|
export declare class GeminiShell implements ShellLauncher {
|
|
8
10
|
private readonly command;
|
|
@@ -10,5 +12,5 @@ export declare class GeminiShell implements ShellLauncher {
|
|
|
10
12
|
readonly injectionMode: InjectionMode;
|
|
11
13
|
constructor(command?: string);
|
|
12
14
|
isAvailable(): Promise<boolean>;
|
|
13
|
-
launch(prompt: string,
|
|
15
|
+
launch(prompt: string, options?: ShellLaunchOptions): Promise<void>;
|
|
14
16
|
}
|
|
@@ -1,18 +1,89 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { exec } from "node:child_process";
|
|
2
3
|
import { promisify } from "node:util";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { homedir, tmpdir } from "node:os";
|
|
4
7
|
import { spawnShell, writeTempPrompt } from "./spawn-shell.js";
|
|
5
|
-
import {
|
|
8
|
+
import { setupGeminiHook, isGeminiHookSetup } from "./gemini-hook.js";
|
|
6
9
|
const execAsync = promisify(exec);
|
|
10
|
+
const RELIC_DIR = join(homedir(), ".relic");
|
|
11
|
+
const GEMINI_DEFAULT_CACHE = join(RELIC_DIR, "gemini-system-default.md");
|
|
12
|
+
const RELIC_ENGRAM_START = "<!-- RELIC ENGRAM START -->";
|
|
13
|
+
const RELIC_ENGRAM_END = "<!-- RELIC ENGRAM END -->";
|
|
14
|
+
/**
|
|
15
|
+
* GEMINI_WRITE_SYSTEM_MD=true で gemini を一時起動し、
|
|
16
|
+
* 組み込みシステムプロンプトをキャプチャして返す。
|
|
17
|
+
* キャプチャ結果は ~/.relic/gemini-system-default.md にキャッシュされる。
|
|
18
|
+
*/
|
|
19
|
+
async function captureDefaultSystemPrompt(command) {
|
|
20
|
+
const tempDir = mkdtempSync(join(tmpdir(), "relic-gemini-capture-"));
|
|
21
|
+
const geminiDir = join(tempDir, ".gemini");
|
|
22
|
+
mkdirSync(geminiDir, { recursive: true });
|
|
23
|
+
try {
|
|
24
|
+
await new Promise((resolve) => {
|
|
25
|
+
const child = spawn(command, [], {
|
|
26
|
+
cwd: tempDir,
|
|
27
|
+
env: { ...process.env, GEMINI_WRITE_SYSTEM_MD: "true" },
|
|
28
|
+
// stdin を /dev/null 相当にして TTY なしで起動
|
|
29
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
30
|
+
});
|
|
31
|
+
// ファイル書き出し猶予として5秒後に強制終了
|
|
32
|
+
const timeout = setTimeout(() => {
|
|
33
|
+
child.kill("SIGTERM");
|
|
34
|
+
}, 5000);
|
|
35
|
+
child.on("close", () => {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
resolve();
|
|
38
|
+
});
|
|
39
|
+
child.on("error", () => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
resolve();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
const systemMdPath = join(geminiDir, "system.md");
|
|
45
|
+
if (!existsSync(systemMdPath)) {
|
|
46
|
+
throw new Error("Failed to capture Gemini default system prompt.\n" +
|
|
47
|
+
"Run manually: GEMINI_WRITE_SYSTEM_MD=true gemini\n" +
|
|
48
|
+
`Then copy .gemini/system.md to ${GEMINI_DEFAULT_CACHE}`);
|
|
49
|
+
}
|
|
50
|
+
const content = readFileSync(systemMdPath, "utf-8");
|
|
51
|
+
mkdirSync(RELIC_DIR, { recursive: true });
|
|
52
|
+
writeFileSync(GEMINI_DEFAULT_CACHE, content, "utf-8");
|
|
53
|
+
return content;
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* デフォルトプロンプトにEngramセクションを追記 or 置換する(冪等)。
|
|
61
|
+
* <!-- RELIC ENGRAM START/END --> デリミタで管理する。
|
|
62
|
+
*/
|
|
63
|
+
function composeSystemPrompt(defaultPrompt, engramPrompt) {
|
|
64
|
+
const engramSection = [
|
|
65
|
+
RELIC_ENGRAM_START,
|
|
66
|
+
engramPrompt,
|
|
67
|
+
RELIC_ENGRAM_END,
|
|
68
|
+
].join("\n");
|
|
69
|
+
if (defaultPrompt.includes(RELIC_ENGRAM_START)) {
|
|
70
|
+
// 既存のRELICセクションを置換
|
|
71
|
+
return defaultPrompt.replace(new RegExp(`${RELIC_ENGRAM_START}[\\s\\S]*?${RELIC_ENGRAM_END}`), engramSection);
|
|
72
|
+
}
|
|
73
|
+
// 末尾に追記
|
|
74
|
+
return `${defaultPrompt}\n\n${engramSection}`;
|
|
75
|
+
}
|
|
7
76
|
/**
|
|
8
77
|
* Gemini CLI アダプター
|
|
9
|
-
*
|
|
10
|
-
*
|
|
78
|
+
* GEMINI_SYSTEM_MD 環境変数でシステムプロンプトを完全上書きし、
|
|
79
|
+
* Engramを組み込みプロンプトと合成して注入する。
|
|
80
|
+
*
|
|
81
|
+
* 初回起動時のみ GEMINI_WRITE_SYSTEM_MD でデフォルトプロンプトをキャプチャ・キャッシュする。
|
|
11
82
|
*/
|
|
12
83
|
export class GeminiShell {
|
|
13
84
|
command;
|
|
14
85
|
name = "Gemini CLI";
|
|
15
|
-
injectionMode = "
|
|
86
|
+
injectionMode = "system-prompt";
|
|
16
87
|
constructor(command = "gemini") {
|
|
17
88
|
this.command = command;
|
|
18
89
|
}
|
|
@@ -25,16 +96,34 @@ export class GeminiShell {
|
|
|
25
96
|
return false;
|
|
26
97
|
}
|
|
27
98
|
}
|
|
28
|
-
async launch(prompt,
|
|
29
|
-
|
|
99
|
+
async launch(prompt, options) {
|
|
100
|
+
// 1. AfterAgent フックを初回のみセットアップ
|
|
101
|
+
if (!isGeminiHookSetup()) {
|
|
102
|
+
console.log("Setting up Gemini AfterAgent hook (first run only)...");
|
|
103
|
+
setupGeminiHook();
|
|
104
|
+
console.log("Hook registered to ~/.gemini/settings.json");
|
|
105
|
+
console.log();
|
|
106
|
+
}
|
|
107
|
+
// 2. デフォルトシステムプロンプトをキャッシュから読む、なければキャプチャ
|
|
108
|
+
let defaultPrompt;
|
|
109
|
+
if (existsSync(GEMINI_DEFAULT_CACHE)) {
|
|
110
|
+
defaultPrompt = readFileSync(GEMINI_DEFAULT_CACHE, "utf-8");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log("Capturing Gemini default system prompt (first run only)...");
|
|
114
|
+
defaultPrompt = await captureDefaultSystemPrompt(this.command);
|
|
115
|
+
console.log(`Cached to: ${GEMINI_DEFAULT_CACHE}`);
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
// 3. デフォルト + Engram を合成してtempファイルに書き出す
|
|
119
|
+
const combined = composeSystemPrompt(defaultPrompt, prompt);
|
|
120
|
+
const tmp = writeTempPrompt(combined);
|
|
30
121
|
try {
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
];
|
|
37
|
-
await spawnShell(this.command, args, cwd);
|
|
122
|
+
// 4. GEMINI_SYSTEM_MD + RELIC_ENGRAM_ID でシステムプロンプトを差し替えて起動
|
|
123
|
+
const env = { GEMINI_SYSTEM_MD: tmp.path };
|
|
124
|
+
if (options?.engramId)
|
|
125
|
+
env.RELIC_ENGRAM_ID = options.engramId;
|
|
126
|
+
await spawnShell(this.command, [...(options?.extraArgs ?? [])], options?.cwd, env);
|
|
38
127
|
}
|
|
39
128
|
finally {
|
|
40
129
|
tmp.cleanup();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* shell: false で起動し、引数をそのまま渡す。
|
|
6
6
|
* これにより長いプロンプト文字列がシェルに壊されない。
|
|
7
7
|
*/
|
|
8
|
-
export declare function spawnShell(command: string, args: string[], cwd?: string): Promise<void>;
|
|
8
|
+
export declare function spawnShell(command: string, args: string[], cwd?: string, env?: Record<string, string>): Promise<void>;
|
|
9
9
|
/**
|
|
10
10
|
* プロンプトをtempファイルに書き出し、パスを返す。
|
|
11
11
|
* Shell終了後にcleanup()を呼んで削除する。
|
|
@@ -9,17 +9,24 @@ import { tmpdir } from "node:os";
|
|
|
9
9
|
* shell: false で起動し、引数をそのまま渡す。
|
|
10
10
|
* これにより長いプロンプト文字列がシェルに壊されない。
|
|
11
11
|
*/
|
|
12
|
-
export function spawnShell(command, args, cwd) {
|
|
12
|
+
export function spawnShell(command, args, cwd, env) {
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
14
14
|
const child = spawn(command, args, {
|
|
15
15
|
stdio: "inherit",
|
|
16
16
|
cwd,
|
|
17
|
+
env: env ? { ...process.env, ...env } : undefined,
|
|
17
18
|
});
|
|
18
19
|
child.on("error", (err) => {
|
|
19
20
|
reject(new Error(`Failed to launch ${command}: ${err.message}`));
|
|
20
21
|
});
|
|
21
|
-
child.on("close", (code) => {
|
|
22
|
-
|
|
22
|
+
child.on("close", (code, signal) => {
|
|
23
|
+
// code 0 = 正常終了
|
|
24
|
+
// code null = シグナルで終了
|
|
25
|
+
// signal SIGINT/SIGTERM = ユーザーによる中断(正常扱い)
|
|
26
|
+
if (code === 0 ||
|
|
27
|
+
code === null ||
|
|
28
|
+
signal === "SIGINT" ||
|
|
29
|
+
signal === "SIGTERM") {
|
|
23
30
|
resolve();
|
|
24
31
|
}
|
|
25
32
|
else {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrustRegistrar — 各Shell CLIに ~/.relic/ ディレクトリの信頼設定を登録する
|
|
3
|
+
*
|
|
4
|
+
* `relic init` 時に一度だけ呼ばれ、以降のセッションで
|
|
5
|
+
* inbox.md 等への書き込みが確認なしで通るようになる。
|
|
6
|
+
*
|
|
7
|
+
* 対応Shell:
|
|
8
|
+
* - Claude Code: ~/.claude/settings.json (sandbox.filesystem.allowWrite)
|
|
9
|
+
* - Codex CLI: ~/.codex/config.toml ([sandbox] writable_roots)
|
|
10
|
+
* - Gemini CLI: ~/.gemini/trustedFolders.json (TRUST_FOLDER)
|
|
11
|
+
*/
|
|
12
|
+
interface TrustResult {
|
|
13
|
+
/** 設定を追加したShell名の一覧 */
|
|
14
|
+
registered: string[];
|
|
15
|
+
/** 既に設定済みでスキップしたShell名の一覧 */
|
|
16
|
+
skipped: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function registerTrustedFolders(relicEngramsPath: string): Promise<TrustResult>;
|
|
19
|
+
export {};
|