@lorekit/cli 1.16.0 → 1.17.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/package.json +1 -1
- package/src/doctor.mjs +2 -1
- package/src/install.mjs +124 -28
- package/src/telemetry.mjs +1 -1
package/package.json
CHANGED
package/src/doctor.mjs
CHANGED
|
@@ -82,8 +82,9 @@ export async function doctor(args) {
|
|
|
82
82
|
// 5. Scope.
|
|
83
83
|
const scope = deriveScope(root);
|
|
84
84
|
if (scope.hasRemote) {
|
|
85
|
+
log('');
|
|
85
86
|
record('info', 'read scope', scope.readOrder.join(' → '));
|
|
86
|
-
record('info', 'write scope', `${scope.repoScope} (default
|
|
87
|
+
record('info', 'write scope', `${scope.repoScope} (default write target)`);
|
|
87
88
|
} else {
|
|
88
89
|
record('warn', 'scope', 'no git remote here — memories fall back to global');
|
|
89
90
|
}
|
package/src/install.mjs
CHANGED
|
@@ -15,21 +15,55 @@ import {
|
|
|
15
15
|
resolveConnection,
|
|
16
16
|
tokenKind,
|
|
17
17
|
homeDir,
|
|
18
|
+
mcpConfigPath,
|
|
19
|
+
readJsonIfExists,
|
|
18
20
|
} from './config.mjs';
|
|
19
|
-
import { buildRemoteUrl } from './mcp.mjs';
|
|
21
|
+
import { buildRemoteUrl, splitEndpoint } from './mcp.mjs';
|
|
20
22
|
import { deriveScope } from './scope.mjs';
|
|
21
|
-
import { log,
|
|
23
|
+
import { log, heading, status, select, c } from './util.mjs';
|
|
24
|
+
|
|
25
|
+
// The MCP server URL is fixed — there is only one hosted LoreKit endpoint.
|
|
26
|
+
const LOREKIT_MCP_ENDPOINT = 'https://pqokxlhvnosogizsjztg.supabase.co/functions/v1/mcp';
|
|
22
27
|
|
|
23
28
|
function ask(question) {
|
|
24
29
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
25
30
|
return new Promise((resolve) => rl.question(question, (a) => { rl.close(); resolve(a.trim()); }));
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
// Detect whether lorekit is already installed for a given scope. Returns an
|
|
34
|
+
// object describing what is present so the caller can give precise feedback.
|
|
35
|
+
function detectInstalled(root, scope) {
|
|
36
|
+
// Check if at least one skill is present.
|
|
37
|
+
const skillsPresent = SKILLS.filter((skill) =>
|
|
38
|
+
fs.existsSync(path.join(skillInstallDir(root, scope, skill.name), 'SKILL.md')),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Check if the MCP server entry exists and extract any configured token.
|
|
42
|
+
// Use a try/catch so a corrupt config file degrades to "not installed"
|
|
43
|
+
// instead of throwing — the user sees a clear error when they actually
|
|
44
|
+
// try to write via upsertMcpServer, which uses the throwing readJsonIfExists.
|
|
45
|
+
const mcpFile = mcpConfigPath(root, scope);
|
|
46
|
+
let mcpConfig = null;
|
|
47
|
+
try { mcpConfig = readJsonIfExists(mcpFile); } catch { /* corrupt — treat as absent */ }
|
|
48
|
+
const mcpServer = mcpConfig && mcpConfig.mcpServers && mcpConfig.mcpServers.lorekit;
|
|
49
|
+
const serverArgs = mcpServer && Array.isArray(mcpServer.args) ? mcpServer.args : [];
|
|
50
|
+
const serverUrl = serverArgs.find((a) => typeof a === 'string' && /^https?:\/\//.test(a));
|
|
51
|
+
const existingToken = serverUrl ? splitEndpoint(serverUrl).token : null;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
hasSkills: skillsPresent.length > 0,
|
|
55
|
+
skillCount: skillsPresent.length,
|
|
56
|
+
totalSkills: SKILLS.length,
|
|
57
|
+
hasMcp: Boolean(mcpServer),
|
|
58
|
+
existingToken,
|
|
59
|
+
isFullyInstalled: skillsPresent.length === SKILLS.length && Boolean(mcpServer),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
29
62
|
|
|
30
63
|
export async function install(args) {
|
|
31
64
|
const root = resolveProjectRoot(args.dir);
|
|
32
65
|
const nonInteractive = Boolean(args.yes) || !process.stdin.isTTY;
|
|
66
|
+
const force = Boolean(args.force);
|
|
33
67
|
|
|
34
68
|
heading('LoreKit install');
|
|
35
69
|
log(` project: ${c.dim(root)}`);
|
|
@@ -47,44 +81,105 @@ export async function install(args) {
|
|
|
47
81
|
]);
|
|
48
82
|
}
|
|
49
83
|
}
|
|
84
|
+
|
|
85
|
+
// 2. Already-installed detection — check both scopes so we can give accurate
|
|
86
|
+
// context ("installed globally but not for this project", etc.).
|
|
87
|
+
const projectState = detectInstalled(root, 'project');
|
|
88
|
+
const globalState = detectInstalled(root, 'global');
|
|
89
|
+
const currentState = scope === 'global' ? globalState : projectState;
|
|
90
|
+
|
|
91
|
+
if (currentState.isFullyInstalled && !force) {
|
|
92
|
+
// Surface a clear, useful already-installed summary.
|
|
93
|
+
log('');
|
|
94
|
+
log(
|
|
95
|
+
` ${c.green('LoreKit is already installed')} for ${
|
|
96
|
+
scope === 'global'
|
|
97
|
+
? 'all projects (global)'
|
|
98
|
+
: 'this project'
|
|
99
|
+
}.`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Cross-scope awareness: tell the user what's installed where.
|
|
103
|
+
if (scope === 'project' && globalState.isFullyInstalled) {
|
|
104
|
+
log(` ${c.dim('Also installed globally — skills and MCP server are active for every project.')}`);
|
|
105
|
+
} else if (scope === 'project' && globalState.hasMcp) {
|
|
106
|
+
log(` ${c.dim('Partially installed globally (MCP server present, but skills may be missing).')}`);
|
|
107
|
+
} else if (scope === 'global' && projectState.isFullyInstalled) {
|
|
108
|
+
log(` ${c.dim('Also installed for this project (.claude, .mcp.json).')}`);
|
|
109
|
+
} else if (scope === 'global' && projectState.hasMcp) {
|
|
110
|
+
log(` ${c.dim('Partially installed for this project (MCP server present, but skills may be missing).')}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Surface the configured token state so the user knows what access they have.
|
|
114
|
+
const kind = tokenKind(currentState.existingToken);
|
|
115
|
+
if (kind === 'read-write') {
|
|
116
|
+
log(` ${c.dim('Token: read+write (lk_rw_*)')}`);
|
|
117
|
+
} else if (kind === 'read-only') {
|
|
118
|
+
log(` ${c.dim('Token: read-only (lk_ro_*) — writes will fail until a read+write token is set')}`);
|
|
119
|
+
} else if (kind === 'write-only') {
|
|
120
|
+
log(` ${c.dim('Token: write-only (lk_wo_*) — reads will fail until a read+write token is set')}`);
|
|
121
|
+
} else if (kind === 'unknown') {
|
|
122
|
+
log(` ${c.dim('Token: unrecognized prefix — expected lk_rw_*, lk_ro_*, or lk_wo_*')}`);
|
|
123
|
+
} else {
|
|
124
|
+
log(` ${c.yellow('Token: none configured — reads/writes will fail until a token is set')}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
log('');
|
|
128
|
+
log(` Run ${c.cyan('npx @lorekit/cli doctor')} to verify the connection.`);
|
|
129
|
+
log(` Pass ${c.cyan('--force')} to reinstall and overwrite existing files.`);
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Partial install — note what's already there vs what will be added.
|
|
134
|
+
if ((currentState.hasSkills || currentState.hasMcp) && !force) {
|
|
135
|
+
const partialNote =
|
|
136
|
+
currentState.hasSkills && !currentState.hasMcp
|
|
137
|
+
? `Skills already present (${currentState.skillCount}/${currentState.totalSkills}) — wiring MCP server.`
|
|
138
|
+
: currentState.hasMcp && !currentState.hasSkills
|
|
139
|
+
? 'MCP server already configured — installing skill files.'
|
|
140
|
+
: `Partially installed (${currentState.skillCount}/${currentState.totalSkills} skills, MCP ${currentState.hasMcp ? 'present' : 'missing'}) — completing setup.`;
|
|
141
|
+
log(`\n ${c.dim(partialNote)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
50
144
|
log(
|
|
51
145
|
` install: ${c.dim(
|
|
52
146
|
scope === 'global' ? 'global — ~/.claude, applies to every project' : 'project — this repo only',
|
|
53
147
|
)}`,
|
|
54
148
|
);
|
|
55
149
|
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
150
|
+
// 3. Connection details.
|
|
151
|
+
// The endpoint is always the fixed hosted LoreKit URL — no need to ask.
|
|
152
|
+
// The token is reused from the existing config when already present; the
|
|
153
|
+
// user only needs to supply it on a fresh install (or to replace it).
|
|
154
|
+
const fromArgs = resolveConnection(args);
|
|
155
|
+
const endpoint = fromArgs.endpoint || LOREKIT_MCP_ENDPOINT;
|
|
156
|
+
|
|
157
|
+
// Token resolution order: --token flag → env → existing config → prompt.
|
|
158
|
+
let token = fromArgs.token;
|
|
159
|
+
if (!token && currentState.existingToken) {
|
|
160
|
+
// Reuse the token that's already in the config — don't make the user repeat
|
|
161
|
+
// it just because they're running install again.
|
|
162
|
+
token = currentState.existingToken;
|
|
163
|
+
log(` ${c.dim('Token: reusing existing token from config.')}`);
|
|
69
164
|
}
|
|
70
165
|
if (!token && !nonInteractive) {
|
|
71
166
|
token = await ask(' LoreKit token (lk_rw_… to allow writes, blank to skip): ');
|
|
72
167
|
token = token || null;
|
|
73
168
|
}
|
|
74
169
|
|
|
75
|
-
//
|
|
170
|
+
// 4. Install the skill files — every skill the CLI ships.
|
|
76
171
|
const skillResults = SKILLS.map((skill) => {
|
|
77
172
|
const dest = skillInstallDir(root, scope, skill.name);
|
|
78
173
|
const existed = fs.existsSync(path.join(dest, 'SKILL.md'));
|
|
79
|
-
const written = copyDir(skill.source, dest, { force
|
|
174
|
+
const written = copyDir(skill.source, dest, { force });
|
|
80
175
|
return { name: skill.name, dest, existed, written };
|
|
81
176
|
});
|
|
82
177
|
|
|
83
|
-
//
|
|
178
|
+
// 5. Wire the MCP config for the chosen scope.
|
|
84
179
|
const remoteUrl = buildRemoteUrl(endpoint, token);
|
|
85
180
|
const { file, existed } = upsertMcpServer(root, remoteUrl, scope);
|
|
86
181
|
|
|
87
|
-
//
|
|
182
|
+
// 5b. Wire the deterministic hooks (unless --no-hooks). This is the layer the
|
|
88
183
|
// Claude plugin adds on top of the skill: lessons injected on every
|
|
89
184
|
// SessionStart, a nudge on tool failure, a retrospective nudge on Stop —
|
|
90
185
|
// firing the shared `lorekit hook` engine, which reads the same config.
|
|
@@ -100,14 +195,14 @@ export async function install(args) {
|
|
|
100
195
|
scope === 'global' ? p.replace(homeDir(), '~') : path.relative(root, p) || p;
|
|
101
196
|
const mcpLabel = scope === 'global' ? '~/.claude.json' : '.mcp.json';
|
|
102
197
|
|
|
103
|
-
//
|
|
198
|
+
// 6. Report.
|
|
104
199
|
heading('Done');
|
|
105
200
|
for (const s of skillResults) {
|
|
106
201
|
const skillState = !s.existed
|
|
107
202
|
? 'installed'
|
|
108
203
|
: s.written > 0
|
|
109
204
|
? `updated (${s.written} file(s) written)`
|
|
110
|
-
: '
|
|
205
|
+
: 'already up to date';
|
|
111
206
|
status(s.existed && s.written === 0 ? 'info' : 'pass', `skill ${s.name}`, `${skillState} → ${display(s.dest)}`);
|
|
112
207
|
}
|
|
113
208
|
status('pass', mcpLabel, `${existed ? 'updated' : 'created'} lorekit server → ${display(file)}`);
|
|
@@ -116,10 +211,11 @@ export async function install(args) {
|
|
|
116
211
|
status('info', 'hooks', 'skipped (--no-hooks) — the skill still works, but memories are model-invoked only');
|
|
117
212
|
} else {
|
|
118
213
|
const n = hooks.added + hooks.updated;
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
214
|
+
const hookParts = [
|
|
215
|
+
hooks.added ? `${hooks.added} added` : '',
|
|
216
|
+
hooks.updated ? `${hooks.updated} updated` : '',
|
|
217
|
+
].filter(Boolean);
|
|
218
|
+
const hookState = n === 0 ? 'already wired' : hookParts.join(', ');
|
|
123
219
|
status(n === 0 ? 'info' : 'pass', 'hooks', `${hookState} → ${display(hooks.file)} (${CLAUDE_HOOK_EVENTS.join(', ')})`);
|
|
124
220
|
}
|
|
125
221
|
|
|
@@ -148,8 +244,8 @@ export async function install(args) {
|
|
|
148
244
|
log(
|
|
149
245
|
` ${c.dim(
|
|
150
246
|
scope === 'global'
|
|
151
|
-
? 'Note: your token
|
|
152
|
-
: 'Note: your token
|
|
247
|
+
? 'Note: your token is stored in ~/.claude.json (used by every project) — keep that file private.'
|
|
248
|
+
: 'Note: your token is stored in .mcp.json — keep it out of version control.',
|
|
153
249
|
)}`,
|
|
154
250
|
);
|
|
155
251
|
}
|
package/src/telemetry.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import { readLorekitJson } from './config.mjs';
|
|
|
32
32
|
// (empty in the source tree, so default export stays off until built/injected).
|
|
33
33
|
const DEFAULT_ENDPOINT = 'https://ingress.europe-west4.gcp.dash0-dev.com';
|
|
34
34
|
const DEFAULT_TOKEN = TELEMETRY_TOKEN; // injected from LOREKIT_TELEMETRY_TOKEN at publish
|
|
35
|
-
const DEFAULT_DATASET = '
|
|
35
|
+
const DEFAULT_DATASET = '';
|
|
36
36
|
|
|
37
37
|
// Flags worth counting (e.g. how many installs are --global). Bounded on
|
|
38
38
|
// purpose: only these booleans are ever attached, never free-form values.
|