@lorekit/cli 1.21.0 → 1.23.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/README.md +10 -10
- package/bin/lorekit.mjs +10 -9
- package/package.json +1 -1
- package/src/mcp-server.mjs +6 -4
- package/src/mcp.mjs +6 -1
- package/src/scopes.mjs +22 -19
- package/src/store/remote.mjs +101 -70
- package/src/telemetry.mjs +38 -12
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ and `global`) — split into two clearly-labelled sections so you can see where
|
|
|
116
116
|
each lesson lives:
|
|
117
117
|
|
|
118
118
|
- **Offline** — the local two-tier store (`.lorekit/` in the repo + `~/.lorekit/`).
|
|
119
|
-
- **Remote** — the hosted
|
|
119
|
+
- **Remote** — the hosted LoreKit store, reached over the REST API. When no token/endpoint is configured this
|
|
120
120
|
section is a short note on how to set it up; it is **never an error** (the
|
|
121
121
|
command still exits 0 and shows your offline lessons). A network/server error
|
|
122
122
|
is likewise a per-scope warning, not a crash.
|
|
@@ -192,7 +192,7 @@ which is the point of an overview). An unconfigured remote degrades to a short
|
|
|
192
192
|
note — never an error, always exit 0. `--endpoint` / `--token` / `--store`
|
|
193
193
|
behave as in `list`. Remote counts reflect what the hosted `memory.list` returns
|
|
194
194
|
per scope (the server's default page size); there is no cap-usage `N / limit`
|
|
195
|
-
figure because the
|
|
195
|
+
figure because the REST API exposes no total-count or cap endpoint.
|
|
196
196
|
|
|
197
197
|
### `lorekit scopes`
|
|
198
198
|
|
|
@@ -224,13 +224,13 @@ stored by basename only) — so every scope is reconstructed verbatim. Lessons
|
|
|
224
224
|
present in both tiers are counted once (project shadows home, the same merge
|
|
225
225
|
`list` uses); archived lessons are excluded.
|
|
226
226
|
|
|
227
|
-
**Remote enumeration is
|
|
228
|
-
|
|
229
|
-
(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
in `list`.
|
|
227
|
+
**Remote enumeration is exact too.** `RemoteStore.listScopes()` calls
|
|
228
|
+
`GET /memories/scopes`, which aggregates one `{ scope, count }` row per scope in
|
|
229
|
+
Postgres (never a truncatable `select('scope')` plus a client-side dedupe), so
|
|
230
|
+
the Remote section is a real inventory rendered through the same helpers as the
|
|
231
|
+
Offline one. A denied, unconfigured, unreachable, or erroring remote degrades to
|
|
232
|
+
a short, accurate note (network error / HTTP status — never a faked listing) at
|
|
233
|
+
exit 0. `--endpoint` / `--token` / `--store` behave as in `list`.
|
|
234
234
|
|
|
235
235
|
### `lorekit diff`
|
|
236
236
|
|
|
@@ -367,7 +367,7 @@ Memory has a controllable backend. Three **modes**:
|
|
|
367
367
|
|------|--------------------|-------|
|
|
368
368
|
| `off` | nowhere | Memory is disabled — every hook event and store op is a silent no-op. |
|
|
369
369
|
| `local` | markdown files in two tiers (see below) | **Local means _not_ on the hosted website** — local lessons never sync to the LoreKit dashboard. That is the point of local: private-by-default, greppable, git-native. |
|
|
370
|
-
| `remote` | the LoreKit
|
|
370
|
+
| `remote` | the hosted LoreKit API (REST) | The shared, cross-machine backend. Reads stay silent until an endpoint + token are configured. This is the default. |
|
|
371
371
|
|
|
372
372
|
### Local store layout — two tiers
|
|
373
373
|
|
package/bin/lorekit.mjs
CHANGED
|
@@ -45,10 +45,10 @@ ${c.bold('Commands')}
|
|
|
45
45
|
the MCP server entry, and the lifecycle hooks for the chosen scope. Surgical —
|
|
46
46
|
other servers, hooks, and settings are left untouched. Prompts
|
|
47
47
|
project vs global; --project / --global choose non-interactively.
|
|
48
|
-
doctor Verify the skill install,
|
|
48
|
+
doctor Verify the skill install, remote connectivity, token, and scope.
|
|
49
49
|
list (ls) List the memories that apply to the current directory, split into
|
|
50
50
|
an Offline section (local .lorekit/ + ~/.lorekit/) and a Remote
|
|
51
|
-
section (hosted
|
|
51
|
+
section (the hosted LoreKit API). Groups by scope (project/branch/repo/global).
|
|
52
52
|
--json for scripting, --scope <s> to narrow.
|
|
53
53
|
search Full-text search the applicable memories across both stores and all
|
|
54
54
|
(grep) scopes (case-insensitive, literal substring over key + value),
|
|
@@ -194,7 +194,7 @@ ${c.bold('Examples')}
|
|
|
194
194
|
${c.bold('Usage')}
|
|
195
195
|
npx @lorekit/cli doctor [options]
|
|
196
196
|
|
|
197
|
-
Checks the node runtime, skill install, resolved memory mode,
|
|
197
|
+
Checks the node runtime, skill install, resolved memory mode, remote connectivity,
|
|
198
198
|
token, and scope.
|
|
199
199
|
|
|
200
200
|
${c.bold('Options')}
|
|
@@ -217,8 +217,8 @@ ${c.bold('Usage')}
|
|
|
217
217
|
|
|
218
218
|
Shows the memories for the scopes that resolve for the current directory
|
|
219
219
|
(project/branch/repo/global), split into an Offline section (the local
|
|
220
|
-
.lorekit/ + ~/.lorekit/ two-tier store) and a Remote section (the hosted
|
|
221
|
-
|
|
220
|
+
.lorekit/ + ~/.lorekit/ two-tier store) and a Remote section (the hosted LoreKit
|
|
221
|
+
API). When no remote token/endpoint is configured the Remote section is a
|
|
222
222
|
short note on how to set it up — never an error.
|
|
223
223
|
|
|
224
224
|
${c.bold('Options')}
|
|
@@ -320,7 +320,7 @@ ${c.bold('Usage')}
|
|
|
320
320
|
|
|
321
321
|
Shows how many memories apply to the current directory's scopes (project/branch/
|
|
322
322
|
repo/global), broken down per scope and per store (Offline = the local .lorekit/
|
|
323
|
-
+ ~/.lorekit/ two-tier store; Remote = the hosted
|
|
323
|
+
+ ~/.lorekit/ two-tier store; Remote = the hosted LoreKit API), with per-store and
|
|
324
324
|
grand totals. An unconfigured remote degrades to a short note, never an error.
|
|
325
325
|
|
|
326
326
|
${c.bold('Options')}
|
|
@@ -348,9 +348,10 @@ is a full inventory — it surfaces scopes anywhere in the store, regardless of
|
|
|
348
348
|
current directory.
|
|
349
349
|
|
|
350
350
|
Offline counts are exact: each scope is read from the memory files' frontmatter,
|
|
351
|
-
not reverse-mapped from the directory layout.
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
not reverse-mapped from the directory layout. Remote counts are exact too — they
|
|
352
|
+
come from \`GET /memories/scopes\`, which aggregates one row per scope server-side.
|
|
353
|
+
A denied, unconfigured, or unreachable remote degrades to a short, accurate note
|
|
354
|
+
rather than an error (exit 0).
|
|
354
355
|
|
|
355
356
|
${c.bold('Options')}
|
|
356
357
|
-d, --dir <path> Target project root (default: current directory)
|
package/package.json
CHANGED
package/src/mcp-server.mjs
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
// off → advertise no tools; a call reports "disabled"
|
|
12
12
|
//
|
|
13
13
|
// org.* tools are always advertised regardless of memory mode. They proxy to
|
|
14
|
-
// the remote endpoint because
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// a
|
|
14
|
+
// the remote endpoint because orgs are server-side state — there is no local
|
|
15
|
+
// equivalent to serve them from. (They no longer require a Supabase JWT: the
|
|
16
|
+
// store calls the REST `/orgs` routes, which accept `lk_*` API tokens as of
|
|
17
|
+
// 00041_org_actor_override.sql.) In local/off mode a transient RemoteStore is
|
|
18
|
+
// built from the configured endpoint + token. If no remote is configured, a
|
|
19
|
+
// clear error is returned.
|
|
18
20
|
//
|
|
19
21
|
// Machine-facing: ONLY JSON-RPC frames go to stdout — any diagnostics go to
|
|
20
22
|
// stderr. The server never throws on malformed or partial input; a bad frame
|
package/src/mcp.mjs
CHANGED
|
@@ -27,7 +27,11 @@ export function buildRemoteUrl(endpoint, token) {
|
|
|
27
27
|
let idCounter = 0;
|
|
28
28
|
|
|
29
29
|
// Returns { ok, httpStatus, result, error, networkError }.
|
|
30
|
-
|
|
30
|
+
//
|
|
31
|
+
// `opts.traceparent` is an optional W3C traceparent header value (see
|
|
32
|
+
// src/telemetry.mjs `getActiveTraceparent`); when present it is forwarded so
|
|
33
|
+
// the server-side span joins the CLI's trace — same idiom as restFetch.
|
|
34
|
+
export async function mcpCall(endpoint, token, method, params = {}, { timeoutMs = 10000, traceparent } = {}) {
|
|
31
35
|
const controller = new AbortController();
|
|
32
36
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
33
37
|
try {
|
|
@@ -40,6 +44,7 @@ export async function mcpCall(endpoint, token, method, params = {}, { timeoutMs
|
|
|
40
44
|
'content-type': 'application/json',
|
|
41
45
|
accept: 'application/json, text/event-stream',
|
|
42
46
|
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
47
|
+
...(traceparent ? { traceparent } : {}),
|
|
43
48
|
},
|
|
44
49
|
body: JSON.stringify({ jsonrpc: '2.0', id: ++idCounter, method, params }),
|
|
45
50
|
signal: controller.signal,
|
package/src/scopes.mjs
CHANGED
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
// mapping the on-disk directory layout, which is lossy for `project::{name}`
|
|
14
14
|
// (stored by basename only) — so every scope is reconstructed exactly.
|
|
15
15
|
//
|
|
16
|
-
// Remote enumeration is
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// Remote enumeration is EXACT too: `RemoteStore.listScopes()` calls
|
|
17
|
+
// `GET /memories/scopes`, which aggregates one row per scope in Postgres (never
|
|
18
|
+
// a truncatable `select('scope')` + client-side dedupe), so both sections are
|
|
19
|
+
// real inventories rendered through the same pure helpers. A denied,
|
|
20
|
+
// unconfigured, unreachable, or erroring remote still degrades to a short,
|
|
21
|
+
// accurate note at exit 0 — never a throw.
|
|
20
22
|
//
|
|
21
23
|
// Graceful, read-only, human-facing (the bin wraps it in `traceCommand`).
|
|
22
24
|
// `LOREKIT_DENY` suppresses a section; `--scope <s>` filters the inventory to
|
|
@@ -26,15 +28,9 @@ import process from 'node:process';
|
|
|
26
28
|
import { resolveProjectRoot } from './config.mjs';
|
|
27
29
|
import { resolveDenies } from './control.mjs';
|
|
28
30
|
import { resolveStores, remoteUnavailableReason } from './stores.mjs';
|
|
29
|
-
import { summarizeScopeInventory, filterScopeInventory } from './lessons-view.mjs';
|
|
31
|
+
import { summarizeScopeInventory, filterScopeInventory, describeError } from './lessons-view.mjs';
|
|
30
32
|
import { log, heading, status, c } from './util.mjs';
|
|
31
33
|
|
|
32
|
-
// The honest note the Remote section shows when it isn't denied/unconfigured:
|
|
33
|
-
// the hosted MCP surface simply can't enumerate scopes. Exported so tests can
|
|
34
|
-
// assert the exact wording rather than a fragile substring.
|
|
35
|
-
export const REMOTE_SCOPES_UNSUPPORTED =
|
|
36
|
-
'remote scope enumeration is not supported by the hosted MCP surface (memory.list requires a scope)';
|
|
37
|
-
|
|
38
34
|
export async function scopes(args) {
|
|
39
35
|
const root = resolveProjectRoot(args.dir);
|
|
40
36
|
const env = { ...process.env };
|
|
@@ -63,16 +59,21 @@ export async function scopes(args) {
|
|
|
63
59
|
offlineSection = { available: true, ...summarizeScopeInventory(inventory) };
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
// Remote:
|
|
67
|
-
//
|
|
68
|
-
//
|
|
62
|
+
// Remote: the hosted enumeration, via `GET /memories/scopes`. Precedence is
|
|
63
|
+
// unchanged from the other read commands — deny first, then connectivity, then
|
|
64
|
+
// the call itself. A failed call degrades to the same bounded, non-PII
|
|
65
|
+
// `describeError` note a failed `list()` gets (network error / HTTP status),
|
|
66
|
+
// never a throw and never a faked inventory.
|
|
69
67
|
let remoteSection;
|
|
70
68
|
if (remoteDenied) {
|
|
71
69
|
remoteSection = { available: false, reason: `disabled by deny constraint (${remoteDenied.source})` };
|
|
72
70
|
} else if (!remote.usable()) {
|
|
73
71
|
remoteSection = { available: false, reason: remoteUnavailableReason(connection) };
|
|
74
72
|
} else {
|
|
75
|
-
|
|
73
|
+
const res = await remote.listScopes();
|
|
74
|
+
remoteSection = res.ok
|
|
75
|
+
? { available: true, ...summarizeScopeInventory(filterScopeInventory(res.scopes, filter)) }
|
|
76
|
+
: { available: false, reason: describeError(res) };
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
if (args.json) {
|
|
@@ -89,15 +90,17 @@ export async function scopes(args) {
|
|
|
89
90
|
log('');
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// Bounded, non-PII telemetry extras (counts +
|
|
93
|
-
// string, path, or token. `remote_available`
|
|
94
|
-
//
|
|
93
|
+
// Bounded, non-PII telemetry extras (counts + booleans) — never a scope
|
|
94
|
+
// string, path, or token. `remote_available` now reflects whether the hosted
|
|
95
|
+
// enumeration actually answered; the remote counts mirror the offline pair.
|
|
95
96
|
return {
|
|
96
97
|
exitCode: 0,
|
|
97
98
|
'lorekit.cli.scopes.offline_scope_count': offlineSection.available ? offlineSection.scopes.length : 0,
|
|
98
99
|
'lorekit.cli.scopes.offline_total': offlineSection.available ? offlineSection.total : 0,
|
|
99
100
|
'lorekit.cli.scopes.filtered': Boolean(filter),
|
|
100
|
-
'lorekit.cli.scopes.
|
|
101
|
+
'lorekit.cli.scopes.remote_scope_count': remoteSection.available ? remoteSection.scopes.length : 0,
|
|
102
|
+
'lorekit.cli.scopes.remote_total': remoteSection.available ? remoteSection.total : 0,
|
|
103
|
+
'lorekit.cli.scopes.remote_available': remoteSection.available,
|
|
101
104
|
};
|
|
102
105
|
}
|
|
103
106
|
|
package/src/store/remote.mjs
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
// Remote store: wraps the LoreKit REST API
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
1
|
+
// Remote store: wraps the LoreKit REST API behind the common store contract.
|
|
2
|
+
// EVERY operation this store performs goes over REST — memory list, search,
|
|
3
|
+
// read, write, delete (soft-archive AND `?force=true` hard-delete), the
|
|
4
|
+
// store-wide `listScopes()` enumeration, all four `org.*` operations, and the
|
|
5
|
+
// `ping` connectivity probe. There is NO MCP transport left here: the store
|
|
6
|
+
// imports `restFetch`/`mcpToRestBase` and nothing else.
|
|
7
|
+
//
|
|
8
|
+
// Org ops used to be the one holdout. The org RPCs resolved their actor from
|
|
9
|
+
// `auth.uid()`, which is NULL on the service-role connection an `lk_*` api_key
|
|
10
|
+
// token gets, so `/orgs` 403'd every CLI caller and the store had to keep a
|
|
11
|
+
// JSON-RPC transport alive purely for `org.create`/`org.list`/`org.rename`/
|
|
12
|
+
// `org.delete`. `00041_org_actor_override.sql` added a `p_actor_user_id`
|
|
13
|
+
// override that the RPCs honour only on a verified service_role connection,
|
|
14
|
+
// and the `orgs` edge function now passes it plus its own tenant filters — so
|
|
15
|
+
// every `/orgs*` route serves api_key tokens and the holdout is gone.
|
|
16
|
+
//
|
|
17
|
+
// `packages/cli/src/mcp.mjs` still exists and is still used: it backs the
|
|
18
|
+
// `lorekit mcp` stdio server command and provides `mcpToRestBase`. It is just
|
|
19
|
+
// no longer a transport for this store.
|
|
20
|
+
// Zero-dependency.
|
|
21
|
+
import { restFetch, mcpToRestBase } from '../mcp.mjs';
|
|
8
22
|
import { getActiveTraceparent } from '../telemetry.mjs';
|
|
9
23
|
|
|
10
24
|
// Drop undefined/null args so JSON payloads stay tidy.
|
|
@@ -20,7 +34,12 @@ export function createRemoteStore({ endpoint, token } = {}) {
|
|
|
20
34
|
|
|
21
35
|
class RemoteStore {
|
|
22
36
|
constructor(endpoint, token) {
|
|
23
|
-
|
|
37
|
+
// The configured endpoint is still spelled as the `/mcp` URL (that is what
|
|
38
|
+
// `.mcp.json` / LOREKIT_MCP_URL hold, and changing that config key is a
|
|
39
|
+
// separate migration). Nothing here speaks MCP: it exists only so
|
|
40
|
+
// `usable()` can judge the configuration and `mcpToRestBase` can derive the
|
|
41
|
+
// REST base from it.
|
|
42
|
+
this.endpoint = endpoint;
|
|
24
43
|
this.token = token;
|
|
25
44
|
this.restBase = mcpToRestBase(endpoint); // REST base URL for memory ops
|
|
26
45
|
this.mode = 'remote';
|
|
@@ -37,22 +56,6 @@ class RemoteStore {
|
|
|
37
56
|
return restFetch(this.restBase, this.token, path, { ...opts, traceparent: this._tp() });
|
|
38
57
|
}
|
|
39
58
|
|
|
40
|
-
async _mcp(name, args) {
|
|
41
|
-
if (!this.usable()) return { ok: false, unusable: true };
|
|
42
|
-
return mcpCall(this.endpoint, this.token, 'tools/call', { name, arguments: args });
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
_mcpEntries(res) {
|
|
46
|
-
if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
|
|
47
|
-
// MCP wraps results in { content: [{ type: 'text', text: '<json>' }] }
|
|
48
|
-
let payload = null;
|
|
49
|
-
if (Array.isArray(res.result?.content)) {
|
|
50
|
-
const text = res.result.content.map((c) => c?.text ?? '').join('');
|
|
51
|
-
try { payload = JSON.parse(text); } catch { /* ignore */ }
|
|
52
|
-
} else { payload = res.result; }
|
|
53
|
-
return { ok: true, entries: Array.isArray(payload?.entries) ? payload.entries : [] };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
59
|
// ── Memory operations → REST ──────────────────────────────────────────────
|
|
57
60
|
|
|
58
61
|
async list({ scope, tags, limit } = {}) {
|
|
@@ -79,6 +82,8 @@ class RemoteStore {
|
|
|
79
82
|
const p = new URLSearchParams();
|
|
80
83
|
if (scope) p.set('scope', scope);
|
|
81
84
|
if (key) p.set('key', key);
|
|
85
|
+
// scope+key is unique, so one row is all there can be — don't pull the default page of 50.
|
|
86
|
+
p.set('limit', '1');
|
|
82
87
|
const res = await this._rest(`/memories?${p}`);
|
|
83
88
|
if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
|
|
84
89
|
const entries = res.data?.entries ?? [];
|
|
@@ -99,14 +104,12 @@ class RemoteStore {
|
|
|
99
104
|
return { ok: res.ok, error: res.error, networkError: res.networkError };
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
// Natural-key DELETE. Without `force` the server soft-archives (stamps
|
|
108
|
+
// `archived_at`); `?force=true` hard-deletes the row outright — both forms of
|
|
109
|
+
// the same REST route (supabase/functions/memories/handlers/remove.ts).
|
|
102
110
|
async delete({ scope, key, force = false } = {}) {
|
|
103
|
-
if (force) {
|
|
104
|
-
// Hard-delete requires MCP — REST only supports soft-archive (archived_at)
|
|
105
|
-
const res = await this._mcp('memory.delete', { scope, key, force: true });
|
|
106
|
-
return { ok: res.ok, error: res.error, networkError: res.networkError };
|
|
107
|
-
}
|
|
108
|
-
// Soft-archive via natural-key REST endpoint
|
|
109
111
|
const p = new URLSearchParams({ scope, key });
|
|
112
|
+
if (force) p.set('force', 'true');
|
|
110
113
|
const res = await this._rest(`/memories?${p}`, { method: 'DELETE' });
|
|
111
114
|
return { ok: res.ok, error: res.error, networkError: res.networkError };
|
|
112
115
|
}
|
|
@@ -116,66 +119,94 @@ class RemoteStore {
|
|
|
116
119
|
return this.delete({ scope, key, force: false });
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// ── Org operations
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
122
|
+
// ── Org operations → REST ─────────────────────────────────────────────────
|
|
123
|
+
// `supabase/functions/orgs/` serves `lk_*` tokens on every route as of
|
|
124
|
+
// 00041_org_actor_override.sql (see the file header). Each method's RETURN
|
|
125
|
+
// SHAPE is unchanged from the MCP era — `packages/cli/src/mcp-server.mjs`
|
|
126
|
+
// serialises these objects straight into a `tools/call` result, so the shape
|
|
127
|
+
// is a published contract, not an internal detail.
|
|
128
|
+
//
|
|
129
|
+
// Slugs are interpolated into the PATH, so they must be encodeURIComponent'd.
|
|
130
|
+
// A slug is server-side constrained to `[a-z0-9-]`, but the CLI passes
|
|
131
|
+
// whatever the user typed and an un-encoded `../` or `?` would retarget the
|
|
132
|
+
// request at a different route entirely.
|
|
133
|
+
|
|
134
|
+
// POST /orgs → 201 with the created org object, `{ id, slug, name, created_at }`.
|
|
135
|
+
// The handler reads the row back after the RPC (`orgs/handlers/orgs/create.ts`);
|
|
136
|
+
// `created_at` is absent only on the read-back miss, where it falls back to
|
|
137
|
+
// `{ id, slug, name }`. A bare JSON id string is the LEGACY shape an older
|
|
138
|
+
// deployed backend can still return — that is what the string branch below is
|
|
139
|
+
// for, not the normal case. Either way this method reassembles the
|
|
140
|
+
// `{ id, slug, name }` triple, because `packages/cli/src/mcp-server.mjs`
|
|
141
|
+
// serialises it straight into a `tools/call` result and that shape is a
|
|
142
|
+
// published contract.
|
|
127
143
|
async orgCreate({ slug, name } = {}) {
|
|
128
|
-
const res = await this.
|
|
144
|
+
const res = await this._rest('/orgs', { method: 'POST', body: { slug, name } });
|
|
129
145
|
if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
try { payload = JSON.parse(res.result.content.map((c) => c?.text ?? '').join('')); } catch {}
|
|
133
|
-
} else { payload = res.result; }
|
|
134
|
-
return { ok: true, org: payload };
|
|
146
|
+
const id = typeof res.data === 'string' ? res.data : (res.data?.id ?? null);
|
|
147
|
+
return { ok: true, org: { id, slug, name } };
|
|
135
148
|
}
|
|
136
149
|
|
|
150
|
+
// GET /orgs → { entries: [{ id, slug, name, role, created_at }] }
|
|
137
151
|
async orgList() {
|
|
138
|
-
const res = await this.
|
|
139
|
-
return
|
|
152
|
+
const res = await this._rest('/orgs');
|
|
153
|
+
if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
|
|
154
|
+
return { ok: true, entries: Array.isArray(res.data?.entries) ? res.data.entries : [] };
|
|
140
155
|
}
|
|
141
156
|
|
|
157
|
+
// PATCH /orgs/:slug → 200 { slug, name }
|
|
142
158
|
async orgRename({ slug, name } = {}) {
|
|
143
|
-
const res = await this.
|
|
159
|
+
const res = await this._rest(`/orgs/${encodeURIComponent(slug)}`, { method: 'PATCH', body: { name } });
|
|
144
160
|
if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
|
|
145
|
-
|
|
146
|
-
try { payload = Array.isArray(res.result?.content) ? JSON.parse(res.result.content.map((c) => c?.text ?? '').join('')) : res.result; } catch {}
|
|
147
|
-
return { ok: true, ...(payload ?? {}) };
|
|
161
|
+
return { ok: true, ...(res.data ?? { slug, name }) };
|
|
148
162
|
}
|
|
149
163
|
|
|
164
|
+
// DELETE /orgs/:slug → 204 with no body, so `deleted: true` is synthesised to
|
|
165
|
+
// preserve the `{ ok, deleted, slug }` shape the MCP tool returned.
|
|
150
166
|
async orgDelete({ slug } = {}) {
|
|
151
|
-
const res = await this.
|
|
167
|
+
const res = await this._rest(`/orgs/${encodeURIComponent(slug)}`, { method: 'DELETE' });
|
|
152
168
|
if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError };
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
169
|
+
return { ok: true, deleted: true, slug };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Store-wide scope enumeration — every distinct scope the caller can see with
|
|
173
|
+
// its count of active (non-archived, non-expired) memories. `GET
|
|
174
|
+
// /memories/scopes` aggregates in Postgres, so the answer is exact at any size
|
|
175
|
+
// (see supabase/functions/memories/handlers/scopes.ts).
|
|
176
|
+
//
|
|
177
|
+
// The `scopes` array is the SAME `[{ scope, count }]` inventory shape
|
|
178
|
+
// `LocalStore.listScopes()` returns, so `scopes.mjs` feeds both through the
|
|
179
|
+
// same pure `filterScopeInventory`/`summarizeScopeInventory` helpers. Ordering
|
|
180
|
+
// is not relied upon (the server sorts by scope asc; the view re-sorts by
|
|
181
|
+
// scope type). Failures use this store's standard `{ ok:false, error,
|
|
182
|
+
// networkError }` envelope so the caller can degrade gracefully.
|
|
162
183
|
async listScopes() {
|
|
163
|
-
|
|
184
|
+
const res = await this._rest('/memories/scopes');
|
|
185
|
+
if (!res.ok) return { ok: false, error: res.error, networkError: res.networkError, unusable: res.unusable };
|
|
186
|
+
const scopes = Array.isArray(res.data?.scopes) ? res.data.scopes : [];
|
|
187
|
+
return { ok: true, scopes: scopes.map((s) => ({ scope: s.scope, count: Number(s.count) || 0 })) };
|
|
164
188
|
}
|
|
165
189
|
|
|
166
190
|
// Connectivity probe for doctor — a transport check, not a memory op.
|
|
191
|
+
//
|
|
192
|
+
// There is no MCP fallback: a `restBase` we could not derive means the
|
|
193
|
+
// configured endpoint is not a URL, and a JSON-RPC POST to that same
|
|
194
|
+
// unparseable string could only fail in a less legible way. Report the
|
|
195
|
+
// configuration problem instead.
|
|
167
196
|
async ping() {
|
|
168
197
|
if (!this.usable()) return { ok: false, unusable: true };
|
|
169
198
|
// Use the /health function as a connectivity probe (public, no auth)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
: null;
|
|
173
|
-
if (healthUrl) {
|
|
174
|
-
try {
|
|
175
|
-
const res = await fetch(healthUrl, { signal: AbortSignal.timeout(5000) });
|
|
176
|
-
return { ok: res.ok, httpStatus: res.status };
|
|
177
|
-
} catch (e) { return { ok: false, networkError: String(e?.message ?? e) }; }
|
|
199
|
+
if (!this.restBase) {
|
|
200
|
+
return { ok: false, error: { message: `Endpoint is not a valid URL: ${this.endpoint}` } };
|
|
178
201
|
}
|
|
179
|
-
|
|
202
|
+
const healthUrl = `${this.restBase.replace(/\/functions\/v1$/, '')}/functions/v1/health`;
|
|
203
|
+
const tp = this._tp();
|
|
204
|
+
try {
|
|
205
|
+
const res = await fetch(healthUrl, {
|
|
206
|
+
signal: AbortSignal.timeout(5000),
|
|
207
|
+
...(tp ? { headers: { traceparent: tp } } : {}),
|
|
208
|
+
});
|
|
209
|
+
return { ok: res.ok, httpStatus: res.status };
|
|
210
|
+
} catch (e) { return { ok: false, networkError: String(e?.message ?? e) }; }
|
|
180
211
|
}
|
|
181
212
|
}
|
package/src/telemetry.mjs
CHANGED
|
@@ -41,19 +41,30 @@ const FLAG_ATTRS = ['global', 'project', 'deep', 'yes', 'force', 'no-hooks', 'js
|
|
|
41
41
|
const OFF_VALUES = new Set(['0', 'off', 'false', 'no', 'disable', 'disabled']);
|
|
42
42
|
|
|
43
43
|
// ── Active trace context (for traceparent propagation to REST calls) ──────────
|
|
44
|
-
// Set at the start of
|
|
45
|
-
//
|
|
44
|
+
// Set at the start of EVERY traced command run — including runs where export is
|
|
45
|
+
// disabled. Context propagation is deliberately decoupled from export: a user
|
|
46
|
+
// who opted out (or who simply has no OTLP endpoint configured) must still get
|
|
47
|
+
// correlated server-side traces, they just don't contribute a CLI span.
|
|
46
48
|
let _activeTraceId = null;
|
|
47
49
|
let _activeSpanId = null;
|
|
50
|
+
// Whether the current command's span will actually be exported. Drives the
|
|
51
|
+
// W3C `sampled` bit only — never whether the header is sent.
|
|
52
|
+
let _activeSampled = false;
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
|
-
* Get the W3C traceparent for the currently-running CLI command, or null
|
|
51
|
-
* Called by RemoteStore to inject the header into
|
|
52
|
-
*
|
|
55
|
+
* Get the W3C traceparent for the currently-running CLI command, or null when
|
|
56
|
+
* no command is running. Called by RemoteStore to inject the header into
|
|
57
|
+
* outgoing REST/MCP calls so the server span joins the CLI's trace.
|
|
58
|
+
*
|
|
59
|
+
* Flags are `01` when the CLI span is exported and `00` when it is not — the
|
|
60
|
+
* trace id is still carried either way, so the server can correlate. This
|
|
61
|
+
* mirrors `formatTraceparent` in `packages/mcp-core/src/trace-context.ts`
|
|
62
|
+
* (the CLI is zero-dep `.mjs` and cannot import the TS module); keep the two
|
|
63
|
+
* byte-consistent.
|
|
53
64
|
*/
|
|
54
65
|
export function getActiveTraceparent() {
|
|
55
66
|
if (!_activeTraceId || !_activeSpanId) return null;
|
|
56
|
-
return `00-${_activeTraceId}-${_activeSpanId}
|
|
67
|
+
return `00-${_activeTraceId}-${_activeSpanId}-${_activeSampled ? '01' : '00'}`;
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
// ── Config resolution ─────────────────────────────────────────────────────────
|
|
@@ -313,18 +324,32 @@ export async function traceCommand(command, args, version, run) {
|
|
|
313
324
|
config = { enabled: false };
|
|
314
325
|
}
|
|
315
326
|
|
|
327
|
+
// Generate trace context BEFORE the export gate below, so outgoing REST/MCP
|
|
328
|
+
// calls can forward it as `traceparent` even when telemetry is disabled.
|
|
329
|
+
// Propagation is decoupled from export: a user with no OTLP endpoint (the
|
|
330
|
+
// common case — TELEMETRY_TOKEN is empty in git) still gets a single
|
|
331
|
+
// correlated server-side trace. The `sampled` bit is what reflects export.
|
|
332
|
+
// The same IDs are reused in exportInvocation() to link the CLI span to
|
|
333
|
+
// REST spans.
|
|
334
|
+
_activeTraceId = randHex(16);
|
|
335
|
+
_activeSpanId = randHex(8);
|
|
336
|
+
_activeSampled = Boolean(config.enabled);
|
|
337
|
+
|
|
316
338
|
// Fast path: no export configured → run with zero telemetry overhead. Still
|
|
317
339
|
// normalize the result to a numeric exit code: commands may resolve to an
|
|
318
340
|
// { exitCode, ...extra } object (e.g. `doctor`), and only the instrumented
|
|
319
341
|
// path below unwraps it. Returning `run()` raw would leak that object all the
|
|
320
342
|
// way to `process.exit(obj)` in the bin entry → ERR_INVALID_ARG_TYPE crash
|
|
321
343
|
// (exit 1) for every user without an OTLP endpoint configured.
|
|
322
|
-
if (!config.enabled)
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
344
|
+
if (!config.enabled) {
|
|
345
|
+
try {
|
|
346
|
+
return normalizeExitCode(await run());
|
|
347
|
+
} finally {
|
|
348
|
+
_activeTraceId = null;
|
|
349
|
+
_activeSpanId = null;
|
|
350
|
+
_activeSampled = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
328
353
|
|
|
329
354
|
const startMs = Date.now();
|
|
330
355
|
let exitCode = 0;
|
|
@@ -389,6 +414,7 @@ export async function traceCommand(command, args, version, run) {
|
|
|
389
414
|
} finally {
|
|
390
415
|
_activeTraceId = null;
|
|
391
416
|
_activeSpanId = null;
|
|
417
|
+
_activeSampled = false;
|
|
392
418
|
}
|
|
393
419
|
}
|
|
394
420
|
}
|