synomem 0.8.0 → 0.9.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/CHANGELOG.md +38 -0
- package/README.md +47 -68
- package/dist/backend.d.ts +18 -6
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +55 -41
- package/dist/backend.js.map +1 -1
- package/dist/cli.d.ts +20 -33
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1391 -1321
- package/dist/cli.js.map +1 -1
- package/dist/configure.d.ts +12 -46
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +51 -192
- package/dist/configure.js.map +1 -1
- package/dist/credentials.d.ts +73 -33
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +167 -43
- package/dist/credentials.js.map +1 -1
- package/dist/discover.d.ts +8 -35
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +42 -38
- package/dist/discover.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/import.d.ts +3 -0
- package/dist/import.d.ts.map +1 -1
- package/dist/import.js +3 -0
- package/dist/import.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts +18 -18
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +402 -231
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp-server.d.ts +5 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +27 -105
- package/dist/mcp-server.js.map +1 -1
- package/dist/oauth.d.ts +31 -33
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +178 -125
- package/dist/oauth.js.map +1 -1
- package/dist/profiles.d.ts +243 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +465 -0
- package/dist/profiles.js.map +1 -0
- package/dist/project.d.ts +8 -39
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +36 -94
- package/dist/project.js.map +1 -1
- package/dist/remote.d.ts +24 -17
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +54 -52
- package/dist/remote.js.map +1 -1
- package/dist/resolvers.d.ts +47 -0
- package/dist/resolvers.d.ts.map +1 -0
- package/dist/resolvers.js +255 -0
- package/dist/resolvers.js.map +1 -0
- package/dist/service.d.ts +3 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/skill-install.d.ts +4 -6
- package/dist/skill-install.d.ts.map +1 -1
- package/dist/skill-install.js +13 -12
- package/dist/skill-install.js.map +1 -1
- package/dist/types.d.ts +45 -15
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +173 -196
- package/docs/mcp.md +69 -65
- package/package.json +1 -1
- package/skills/synomem/SKILL.md +29 -26
- package/skills/synomem/references/examples.md +13 -11
- package/src/backend.ts +66 -64
- package/src/cli.ts +2131 -2222
- package/src/configure.ts +62 -241
- package/src/credentials.ts +208 -84
- package/src/discover.ts +53 -59
- package/src/errors.ts +4 -0
- package/src/import.ts +5 -0
- package/src/index.ts +15 -19
- package/src/mcp/index.ts +473 -277
- package/src/mcp-server.ts +32 -114
- package/src/oauth.ts +229 -130
- package/src/profiles.ts +644 -0
- package/src/project.ts +42 -108
- package/src/remote.ts +69 -63
- package/src/resolvers.ts +299 -0
- package/src/service.ts +2 -7
- package/src/skill-install.ts +17 -18
- package/src/types.ts +40 -15
package/src/resolvers.ts
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context resolvers: the one place an operation's workspace/actor target is chosen
|
|
3
|
+
* (identity contract §6, §6.1; plan §7 "Implementation boundary").
|
|
4
|
+
*
|
|
5
|
+
* Every MCP tool and CLI command asks a resolver for a service bound to exactly one
|
|
6
|
+
* context, per call. Nothing holds a mutable "current actor" or "current workspace":
|
|
7
|
+
* a resolved binding is immutable, so interleaved calls for two contexts cannot affect
|
|
8
|
+
* each other's attribution. The resolver narrows (pins a context, restricts to a
|
|
9
|
+
* preset); the server alone authorizes.
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { SynomemClient } from './client.js';
|
|
13
|
+
import { describeIdentity, discoverContexts } from './discover.js';
|
|
14
|
+
import { SynomemError } from './errors.js';
|
|
15
|
+
import { bearerFrom, RemoteSynomemService } from './remote.js';
|
|
16
|
+
import type { RemoteCredential, RemoteCredentialSource } from './remote.js';
|
|
17
|
+
import type { SynomemService } from './service.js';
|
|
18
|
+
import type {
|
|
19
|
+
ActorIdentity,
|
|
20
|
+
ContextListing,
|
|
21
|
+
ContextSummary,
|
|
22
|
+
EffectiveContext,
|
|
23
|
+
IdentityDescription,
|
|
24
|
+
SynomemClientOptions,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
|
|
27
|
+
export type { RemoteCredentialSource } from './remote.js';
|
|
28
|
+
|
|
29
|
+
export interface ResolvedContext {
|
|
30
|
+
service: SynomemService;
|
|
31
|
+
context: EffectiveContext;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ContextResolver {
|
|
35
|
+
/** `fixed`: omitting a context selects the one pinned target. */
|
|
36
|
+
mode(): 'fixed' | 'explicit' | 'unknown';
|
|
37
|
+
/** Resolve one operation's binding. `undefined` or `'default'` selects the fixed context. */
|
|
38
|
+
resolve(contextId?: string): Promise<ResolvedContext>;
|
|
39
|
+
list(): Promise<ContextListing>;
|
|
40
|
+
describe?(): Promise<IdentityDescription | undefined>;
|
|
41
|
+
close?(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The selector a model may pass to mean "the fixed context", e.g. in resource URIs. */
|
|
45
|
+
export const DEFAULT_CONTEXT = 'default';
|
|
46
|
+
|
|
47
|
+
const listingTtlMs = 30_000;
|
|
48
|
+
|
|
49
|
+
function normalize(contextId: string | undefined): string | undefined {
|
|
50
|
+
const value = contextId?.trim();
|
|
51
|
+
return !value || value === DEFAULT_CONTEXT ? undefined : value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function forbidden(): SynomemError {
|
|
55
|
+
// The same message whether the target is missing or merely unpermitted: an
|
|
56
|
+
// authorization failure must not reveal that an inaccessible context exists.
|
|
57
|
+
return new SynomemError(
|
|
58
|
+
'CONTEXT_FORBIDDEN',
|
|
59
|
+
'That context is not available to this credential. Call synomem_context_list to see the ones that are.',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function required(): SynomemError {
|
|
64
|
+
return new SynomemError(
|
|
65
|
+
'CONTEXT_REQUIRED',
|
|
66
|
+
'This connection can act as more than one workspace/actor. Pass contextId: call synomem_context_list and choose the context that matches what the user asked for.',
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Stable local context id: derived from the store's persistent workspace id and the
|
|
72
|
+
* canonical actor — never from a display name — so it survives renames and moving the
|
|
73
|
+
* store, and an unrelated store with a same-named agent gets a different id.
|
|
74
|
+
*/
|
|
75
|
+
export function localContextId(storeWorkspaceId: string, actor: ActorIdentity): string {
|
|
76
|
+
const digest = createHash('sha256')
|
|
77
|
+
.update(`${storeWorkspaceId}\0${actor.kind}\0${actor.id}`)
|
|
78
|
+
.digest('hex');
|
|
79
|
+
return `lctx_${digest.slice(0, 24)}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A remote (hosted API) resolver over one credential.
|
|
84
|
+
*
|
|
85
|
+
* `pinnedContextId` makes a fixed profile: only that target, whatever else the grant
|
|
86
|
+
* allows. `allowedContextIds` makes an explicit preset: only those targets may be
|
|
87
|
+
* selected. Either way the API re-authorizes every request; these only narrow.
|
|
88
|
+
*/
|
|
89
|
+
export function createRemoteResolver(options: {
|
|
90
|
+
baseUrl: string;
|
|
91
|
+
credential: RemoteCredentialSource | RemoteCredential;
|
|
92
|
+
pinnedContextId?: string;
|
|
93
|
+
allowedContextIds?: string[];
|
|
94
|
+
fetch?: typeof fetch;
|
|
95
|
+
}): ContextResolver {
|
|
96
|
+
const bearer = bearerFrom(options.credential);
|
|
97
|
+
const pinned = normalize(options.pinnedContextId);
|
|
98
|
+
const allowed = options.allowedContextIds?.length
|
|
99
|
+
? new Set(options.allowedContextIds)
|
|
100
|
+
: undefined;
|
|
101
|
+
let cached: { listing: ContextListing; at: number } | undefined;
|
|
102
|
+
const services = new Map<string, Promise<RemoteSynomemService>>();
|
|
103
|
+
|
|
104
|
+
const accessToken = async (): Promise<string> => {
|
|
105
|
+
const token = await bearer();
|
|
106
|
+
if (!token)
|
|
107
|
+
throw new SynomemError('AUTH_REQUIRED', 'Remote Synomem authentication is required.');
|
|
108
|
+
return token;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const serverListing = async (fresh = false): Promise<ContextListing> => {
|
|
112
|
+
if (!fresh && cached && Date.now() - cached.at < listingTtlMs) return cached.listing;
|
|
113
|
+
const listing = await discoverContexts({
|
|
114
|
+
baseUrl: options.baseUrl,
|
|
115
|
+
accessToken: await accessToken(),
|
|
116
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
117
|
+
});
|
|
118
|
+
cached = { listing, at: Date.now() };
|
|
119
|
+
return listing;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const visible = (listing: ContextListing): ContextListing => {
|
|
123
|
+
if (pinned) {
|
|
124
|
+
return {
|
|
125
|
+
mode: 'fixed',
|
|
126
|
+
fixedContextId: pinned,
|
|
127
|
+
contexts: listing.contexts.filter((entry) => entry.contextId === pinned),
|
|
128
|
+
nextCursor: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (allowed) {
|
|
132
|
+
return {
|
|
133
|
+
mode: 'explicit',
|
|
134
|
+
fixedContextId: null,
|
|
135
|
+
contexts: listing.contexts.filter((entry) => allowed.has(entry.contextId)),
|
|
136
|
+
nextCursor: null,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return listing;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const summaryFor = async (contextId: string): Promise<ContextSummary> => {
|
|
143
|
+
let found = (await serverListing()).contexts.find((entry) => entry.contextId === contextId);
|
|
144
|
+
// A newly eligible target (a future-workspace rule, a fresh binding) may postdate the
|
|
145
|
+
// cached listing; look once more before refusing.
|
|
146
|
+
if (!found) {
|
|
147
|
+
found = (await serverListing(true)).contexts.find((entry) => entry.contextId === contextId);
|
|
148
|
+
}
|
|
149
|
+
if (!found) throw forbidden();
|
|
150
|
+
return found;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const serviceFor = (summary: ContextSummary): Promise<RemoteSynomemService> => {
|
|
154
|
+
let pending = services.get(summary.contextId);
|
|
155
|
+
if (!pending) {
|
|
156
|
+
pending = (async () => {
|
|
157
|
+
const service = new RemoteSynomemService({
|
|
158
|
+
baseUrl: options.baseUrl,
|
|
159
|
+
workspaceId: summary.workspaceId,
|
|
160
|
+
contextId: summary.contextId,
|
|
161
|
+
credential: options.credential,
|
|
162
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
163
|
+
});
|
|
164
|
+
await service.init();
|
|
165
|
+
return service;
|
|
166
|
+
})();
|
|
167
|
+
services.set(summary.contextId, pending);
|
|
168
|
+
pending.catch(() => services.delete(summary.contextId));
|
|
169
|
+
}
|
|
170
|
+
return pending;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
mode() {
|
|
175
|
+
if (pinned) return 'fixed';
|
|
176
|
+
if (allowed) return 'explicit';
|
|
177
|
+
return cached ? cached.listing.mode : 'unknown';
|
|
178
|
+
},
|
|
179
|
+
async resolve(contextId) {
|
|
180
|
+
const requested = normalize(contextId);
|
|
181
|
+
let target: string;
|
|
182
|
+
if (pinned) {
|
|
183
|
+
if (requested && requested !== pinned) throw forbidden();
|
|
184
|
+
target = pinned;
|
|
185
|
+
} else if (requested) {
|
|
186
|
+
if (allowed && !allowed.has(requested)) throw forbidden();
|
|
187
|
+
target = requested;
|
|
188
|
+
} else {
|
|
189
|
+
const listing = await serverListing();
|
|
190
|
+
if (listing.mode === 'fixed' && listing.fixedContextId && !allowed) {
|
|
191
|
+
target = listing.fixedContextId;
|
|
192
|
+
} else {
|
|
193
|
+
throw required();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const summary = await summaryFor(target);
|
|
197
|
+
const service = await serviceFor(summary);
|
|
198
|
+
return {
|
|
199
|
+
service,
|
|
200
|
+
context: {
|
|
201
|
+
contextId: summary.contextId,
|
|
202
|
+
organizationId: summary.organizationId,
|
|
203
|
+
workspaceId: summary.workspaceId,
|
|
204
|
+
actor: service.actor,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
async list() {
|
|
209
|
+
return visible(await serverListing());
|
|
210
|
+
},
|
|
211
|
+
async describe() {
|
|
212
|
+
return await describeIdentity({
|
|
213
|
+
baseUrl: options.baseUrl,
|
|
214
|
+
accessToken: await accessToken(),
|
|
215
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
216
|
+
});
|
|
217
|
+
},
|
|
218
|
+
async close() {
|
|
219
|
+
const pending = [...services.values()];
|
|
220
|
+
services.clear();
|
|
221
|
+
await Promise.allSettled(pending.map(async (service) => (await service).close()));
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A local (SQLite) resolver: fixed mode, one store, one canonical actor.
|
|
228
|
+
*/
|
|
229
|
+
export function createLocalResolver(
|
|
230
|
+
options: { home: string; actor: ActorIdentity } & Omit<SynomemClientOptions, 'home' | 'actor'>,
|
|
231
|
+
): ContextResolver {
|
|
232
|
+
let ready: Promise<ResolvedContext> | undefined;
|
|
233
|
+
const open = (): Promise<ResolvedContext> => {
|
|
234
|
+
ready ??= (async () => {
|
|
235
|
+
const client = new SynomemClient({ ...options, home: options.home, actor: options.actor });
|
|
236
|
+
await client.init();
|
|
237
|
+
const capabilities = await client.capabilities();
|
|
238
|
+
// The canonical actor (a handle resolves to its opaque id), never the requested one.
|
|
239
|
+
const actor = client.actor;
|
|
240
|
+
return {
|
|
241
|
+
service: client,
|
|
242
|
+
context: {
|
|
243
|
+
contextId: localContextId(capabilities.binding.workspaceId, actor),
|
|
244
|
+
organizationId: null,
|
|
245
|
+
workspaceId: capabilities.binding.workspaceId,
|
|
246
|
+
actor,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
})();
|
|
250
|
+
ready.catch(() => {
|
|
251
|
+
ready = undefined;
|
|
252
|
+
});
|
|
253
|
+
return ready;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
mode: () => 'fixed',
|
|
258
|
+
async resolve(contextId) {
|
|
259
|
+
const bound = await open();
|
|
260
|
+
const requested = normalize(contextId);
|
|
261
|
+
if (requested && requested !== bound.context.contextId) throw forbidden();
|
|
262
|
+
return bound;
|
|
263
|
+
},
|
|
264
|
+
async list() {
|
|
265
|
+
const { context } = await open();
|
|
266
|
+
return {
|
|
267
|
+
mode: 'fixed',
|
|
268
|
+
fixedContextId: context.contextId,
|
|
269
|
+
contexts: [
|
|
270
|
+
{
|
|
271
|
+
contextId: context.contextId,
|
|
272
|
+
organizationId: null,
|
|
273
|
+
workspaceId: context.workspaceId,
|
|
274
|
+
actor: context.actor,
|
|
275
|
+
actions: ['synomem:read', 'synomem:write'],
|
|
276
|
+
source: 'local',
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
nextCursor: null,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
async describe() {
|
|
283
|
+
const { context } = await open();
|
|
284
|
+
return {
|
|
285
|
+
account: { id: 'local' },
|
|
286
|
+
organizationId: null,
|
|
287
|
+
connection: { id: 'local', label: options.home, kind: 'local' },
|
|
288
|
+
grant: null,
|
|
289
|
+
fixedContextId: context.contextId,
|
|
290
|
+
effectiveContext: context,
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
async close() {
|
|
294
|
+
const pending = ready;
|
|
295
|
+
ready = undefined;
|
|
296
|
+
if (pending) await (await pending.catch(() => undefined))?.service.close();
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -48,7 +48,6 @@ import type {
|
|
|
48
48
|
UpdateTopicInput,
|
|
49
49
|
TopicListInput,
|
|
50
50
|
TopicResolution,
|
|
51
|
-
WorkspaceIdentity,
|
|
52
51
|
} from './types.js';
|
|
53
52
|
|
|
54
53
|
export interface SynomemServiceCapabilities {
|
|
@@ -56,6 +55,8 @@ export interface SynomemServiceCapabilities {
|
|
|
56
55
|
binding: {
|
|
57
56
|
workspaceId: string;
|
|
58
57
|
actor: ActorIdentity;
|
|
58
|
+
/** The stable context this service is bound to, when the backend reports one. */
|
|
59
|
+
contextId?: string;
|
|
59
60
|
};
|
|
60
61
|
administration: {
|
|
61
62
|
agentCreationViaMcp: boolean;
|
|
@@ -245,12 +246,6 @@ export interface SynomemService extends SynomemDomainService {
|
|
|
245
246
|
capabilities(): Promise<SynomemServiceCapabilities>;
|
|
246
247
|
info(): Promise<SynomemServiceInfo>;
|
|
247
248
|
getCanonicalEvent(id: string): Promise<SynomemEvent | undefined>;
|
|
248
|
-
/*
|
|
249
|
-
* Optional, because only a hosted Synomem Cloud backend has an
|
|
250
|
-
* organization with more than one workspace an account can belong to.
|
|
251
|
-
* Local SQLite has no membership model to report on.
|
|
252
|
-
*/
|
|
253
|
-
workspaces?(): Promise<WorkspaceIdentity>;
|
|
254
249
|
}
|
|
255
250
|
|
|
256
251
|
export type SynomemServiceFactory = (options: SynomemClientOptions) => SynomemService;
|
package/src/skill-install.ts
CHANGED
|
@@ -14,7 +14,6 @@ import { homedir } from 'node:os';
|
|
|
14
14
|
import { dirname, join, resolve } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { SynomemError } from './errors.js';
|
|
17
|
-
import { actorSchema } from './schemas.js';
|
|
18
17
|
import { packageVersion } from './version.js';
|
|
19
18
|
|
|
20
19
|
export const skillRuntimeNames = [
|
|
@@ -58,13 +57,11 @@ export interface SkillOptions {
|
|
|
58
57
|
force?: boolean;
|
|
59
58
|
link?: boolean;
|
|
60
59
|
/**
|
|
61
|
-
* The
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* The display name and kind are read from the agent's profile at startup, so
|
|
65
|
-
* a harness cannot sign another agent's name to work it did.
|
|
60
|
+
* The profile the generated MCP registration launches (`synomem mcp
|
|
61
|
+
* --profile <name>`). The profile, not the command line, decides the acting
|
|
62
|
+
* identity, so a harness cannot name another agent's identity itself.
|
|
66
63
|
*/
|
|
67
|
-
|
|
64
|
+
profile?: string;
|
|
68
65
|
userHome?: string;
|
|
69
66
|
env?: NodeJS.ProcessEnv;
|
|
70
67
|
source?: string;
|
|
@@ -166,24 +163,26 @@ function shellQuote(value: string): string {
|
|
|
166
163
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
167
164
|
}
|
|
168
165
|
|
|
169
|
-
function mcpCommand(runtime: SkillRuntime,
|
|
170
|
-
if (!
|
|
171
|
-
|
|
172
|
-
|
|
166
|
+
function mcpCommand(runtime: SkillRuntime, profile?: string): string | undefined {
|
|
167
|
+
if (!profile) return undefined;
|
|
168
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,62}$/i.test(profile)) {
|
|
169
|
+
throw new SynomemError('INVALID_INPUT', `Invalid profile name: ${profile}`);
|
|
170
|
+
}
|
|
171
|
+
const args = ['mcp', '--profile', profile];
|
|
173
172
|
if (runtime === 'codex') {
|
|
174
|
-
return `codex mcp add synomem -- synomem
|
|
173
|
+
return `codex mcp add synomem -- synomem ${args.map(shellQuote).join(' ')}`;
|
|
175
174
|
}
|
|
176
175
|
if (runtime === 'claude') {
|
|
177
|
-
return `claude mcp add --scope user synomem -- synomem
|
|
176
|
+
return `claude mcp add --scope user synomem -- synomem ${args.map(shellQuote).join(' ')}`;
|
|
178
177
|
}
|
|
179
178
|
if (runtime === 'hermes') {
|
|
180
|
-
return `hermes mcp add synomem --command synomem
|
|
179
|
+
return `hermes mcp add synomem --command synomem --args ${args.map(shellQuote).join(' ')}`;
|
|
181
180
|
}
|
|
182
181
|
if (runtime === 'openclaw') {
|
|
183
|
-
return `openclaw mcp add synomem --command synomem
|
|
182
|
+
return `openclaw mcp add synomem --command synomem ${args.map((item) => `--arg=${shellQuote(item)}`).join(' ')}`;
|
|
184
183
|
}
|
|
185
184
|
if (runtime === 'grok') {
|
|
186
|
-
return `grok mcp add synomem -- synomem
|
|
185
|
+
return `grok mcp add synomem -- synomem ${args.map(shellQuote).join(' ')}`;
|
|
187
186
|
}
|
|
188
187
|
return undefined;
|
|
189
188
|
}
|
|
@@ -255,7 +254,7 @@ export function skillStatus(options: SkillOptions = {}): SkillOperationResult {
|
|
|
255
254
|
packageVersion: packageVersion(),
|
|
256
255
|
locations,
|
|
257
256
|
mcpCommands: locations
|
|
258
|
-
.map((location) => mcpCommand(location.runtime, options.
|
|
257
|
+
.map((location) => mcpCommand(location.runtime, options.profile))
|
|
259
258
|
.filter((value): value is string => Boolean(value)),
|
|
260
259
|
};
|
|
261
260
|
}
|
|
@@ -323,7 +322,7 @@ export function formatSkillResult(
|
|
|
323
322
|
lines.push('', 'Dry run only. Re-run with --yes to apply.');
|
|
324
323
|
if (result.mcpCommands.length) lines.push('', 'MCP registration:', ...result.mcpCommands);
|
|
325
324
|
else if (operation === 'install') {
|
|
326
|
-
lines.push('', 'Tip: add --
|
|
325
|
+
lines.push('', 'Tip: add --profile <name> to print MCP registration commands.');
|
|
327
326
|
}
|
|
328
327
|
return lines.join('\n');
|
|
329
328
|
}
|
package/src/types.ts
CHANGED
|
@@ -822,25 +822,50 @@ export interface DoctorResult {
|
|
|
822
822
|
diagnostics: Diagnostic[];
|
|
823
823
|
}
|
|
824
824
|
/**
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
* `addressableWithThisToken: true` entry can be reached on the very next
|
|
829
|
-
* request by sending it as `Synomem-Workspace-Id` — no re-authentication.
|
|
830
|
-
* An agent-bound token has only ever one such entry, its own.
|
|
825
|
+
* The one workspace/actor target an operation actually ran as (identity contract §4).
|
|
826
|
+
* `contextId` is a stable, grant-independent identifier: `ctx_…` for a hosted target,
|
|
827
|
+
* `lctx_…` for a local SQLite target.
|
|
831
828
|
*/
|
|
832
|
-
export interface
|
|
829
|
+
export interface EffectiveContext {
|
|
830
|
+
contextId: string;
|
|
831
|
+
organizationId: string | null;
|
|
832
|
+
workspaceId: string;
|
|
833
833
|
actor: ActorIdentity;
|
|
834
|
-
|
|
834
|
+
connectionId?: string;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** One entry of `GET /v1/contexts`: a target this credential may currently use. */
|
|
838
|
+
export interface ContextSummary {
|
|
839
|
+
contextId: string;
|
|
840
|
+
organizationId: string | null;
|
|
835
841
|
workspaceId: string;
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
842
|
+
workspaceName?: string;
|
|
843
|
+
actor: ActorIdentity & { handle?: string };
|
|
844
|
+
actions: string[];
|
|
845
|
+
source: 'target' | 'rule' | 'local';
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** `GET /v1/contexts`. `fixed` means omitting a context selects `fixedContextId`. */
|
|
849
|
+
export interface ContextListing {
|
|
850
|
+
mode: 'fixed' | 'explicit';
|
|
851
|
+
fixedContextId: string | null;
|
|
852
|
+
contexts: ContextSummary[];
|
|
853
|
+
nextCursor?: string | null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/** `GET /v1/identity`: the credential, its grant, and (optionally) the effective context. */
|
|
857
|
+
export interface IdentityDescription {
|
|
858
|
+
account: { id: string };
|
|
859
|
+
organizationId: string | null;
|
|
860
|
+
connection: { id: string; label: string; kind: 'oauth' | 'access_key' | 'local' } | null;
|
|
861
|
+
grant: {
|
|
839
862
|
id: string;
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
}
|
|
863
|
+
version: number;
|
|
864
|
+
mode: 'fixed' | 'explicit';
|
|
865
|
+
actions: string[];
|
|
866
|
+
} | null;
|
|
867
|
+
fixedContextId: string | null;
|
|
868
|
+
effectiveContext: EffectiveContext | null;
|
|
844
869
|
}
|
|
845
870
|
/**
|
|
846
871
|
* What the projected files on disk look like next to what they should be.
|