synomem 0.3.0 → 0.4.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 +40 -0
- package/README.md +4 -4
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +8 -2
- package/dist/backend.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +280 -16
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +32 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +125 -10
- package/dist/client.js.map +1 -1
- package/dist/cloud.d.ts +16 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +19 -0
- package/dist/cloud.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -1
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts +55 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +177 -0
- package/dist/configure.js.map +1 -0
- package/dist/credentials.d.ts +15 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js.map +1 -1
- package/dist/import.d.ts +58 -43
- package/dist/import.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +15 -6
- package/dist/mcp/index.js.map +1 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +8 -1
- package/dist/oauth.js.map +1 -1
- package/dist/projections.js +4 -4
- package/dist/projections.js.map +1 -1
- package/dist/prompt.d.ts +28 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +72 -0
- package/dist/prompt.js.map +1 -0
- package/dist/remote.d.ts +4 -0
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +4 -0
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +101 -60
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +57 -5
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +4 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +14 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +55 -14
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +24 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/cli.md +1 -1
- package/docs/examples.md +1 -1
- package/docs/mcp.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/storage-format.md +1 -1
- package/package.json +8 -8
- package/src/backend.ts +8 -2
- package/src/cli.ts +377 -19
- package/src/client.ts +135 -11
- package/src/cloud.ts +19 -0
- package/src/config.ts +8 -1
- package/src/configure.ts +233 -0
- package/src/credentials.ts +17 -2
- package/src/index.ts +7 -1
- package/src/mcp/index.ts +19 -5
- package/src/oauth.ts +8 -1
- package/src/projections.ts +4 -4
- package/src/prompt.ts +88 -0
- package/src/remote.ts +24 -0
- package/src/schemas.ts +60 -5
- package/src/service.ts +4 -0
- package/src/storage.ts +70 -13
- package/src/types.ts +24 -2
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small prompt boundary, injectable so the wizard is testable.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not a dependency: the wizard needs a select, a line, a masked
|
|
5
|
+
* line and a confirm, and a library for that would be more surface than
|
|
6
|
+
* substance. Everything reads from an injected stream, so tests drive the flow
|
|
7
|
+
* without a terminal.
|
|
8
|
+
*/
|
|
9
|
+
import { createInterface } from 'node:readline/promises';
|
|
10
|
+
import type { Readable, Writable } from 'node:stream';
|
|
11
|
+
|
|
12
|
+
export interface PromptIo {
|
|
13
|
+
input: Readable;
|
|
14
|
+
output: Writable;
|
|
15
|
+
/**
|
|
16
|
+
* Whether a person is actually there. A wizard must never wait forever on a
|
|
17
|
+
* pipe, so a non-interactive stream is refused with instructions instead.
|
|
18
|
+
*/
|
|
19
|
+
interactive: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function defaultPromptIo(): PromptIo {
|
|
23
|
+
return {
|
|
24
|
+
input: process.stdin,
|
|
25
|
+
output: process.stdout,
|
|
26
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function readLine(io: PromptIo, question: string): Promise<string> {
|
|
31
|
+
const rl = createInterface({ input: io.input, output: io.output, terminal: io.interactive });
|
|
32
|
+
try {
|
|
33
|
+
return (await rl.question(question)).trim();
|
|
34
|
+
} finally {
|
|
35
|
+
rl.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function ask(io: PromptIo, question: string, fallback?: string): Promise<string> {
|
|
40
|
+
const suffix = fallback ? ` [${fallback}]` : '';
|
|
41
|
+
const answer = await readLine(io, `${question}${suffix}: `);
|
|
42
|
+
return answer || fallback || '';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function confirm(io: PromptIo, question: string): Promise<boolean> {
|
|
46
|
+
const answer = await readLine(io, `${question} [y/N]: `);
|
|
47
|
+
return /^y(es)?$/i.test(answer);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads a secret from a non-interactive stream, or prompts for one.
|
|
52
|
+
*
|
|
53
|
+
* Piping a secret in is the SAFE path and is why this accepts a closed stream
|
|
54
|
+
* rather than refusing it: a token passed as a command-line argument is kept by
|
|
55
|
+
* both the shell history and the process list, so `--access-token-stdin` has to
|
|
56
|
+
* work without a terminal.
|
|
57
|
+
*/
|
|
58
|
+
export async function askSecret(io: PromptIo, question: string): Promise<string> {
|
|
59
|
+
if (!io.interactive) {
|
|
60
|
+
const chunks: Buffer[] = [];
|
|
61
|
+
for await (const chunk of io.input) chunks.push(Buffer.from(chunk as Uint8Array));
|
|
62
|
+
return Buffer.concat(chunks).toString('utf8').trim();
|
|
63
|
+
}
|
|
64
|
+
// Readline echoes, so an interactive secret is read the same way and the
|
|
65
|
+
// caller is told not to expect masking rather than being silently exposed.
|
|
66
|
+
io.output.write('The value you type will be visible. Paste it, or pipe it in instead.\n');
|
|
67
|
+
return await readLine(io, `${question}: `);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function select<T extends string>(
|
|
71
|
+
io: PromptIo,
|
|
72
|
+
question: string,
|
|
73
|
+
choices: Array<{ value: T; label: string; detail?: string }>,
|
|
74
|
+
): Promise<T> {
|
|
75
|
+
io.output.write(`\n${question}\n\n`);
|
|
76
|
+
choices.forEach((choice, index) => {
|
|
77
|
+
io.output.write(` ${index + 1}) ${choice.label}\n`);
|
|
78
|
+
if (choice.detail) io.output.write(` ${choice.detail}\n`);
|
|
79
|
+
});
|
|
80
|
+
io.output.write('\n');
|
|
81
|
+
for (;;) {
|
|
82
|
+
const answer = await readLine(io, `Choose 1-${choices.length} [1]: `);
|
|
83
|
+
const index = Number(answer || '1');
|
|
84
|
+
const choice = choices[index - 1];
|
|
85
|
+
if (choice) return choice.value;
|
|
86
|
+
io.output.write('Enter one of the listed numbers.\n');
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/remote.ts
CHANGED
|
@@ -179,6 +179,30 @@ export class RemoteSynomemService implements SynomemService {
|
|
|
179
179
|
'GET',
|
|
180
180
|
`agents/resolve?query=${encodeURIComponent(query)}`,
|
|
181
181
|
),
|
|
182
|
+
archive: (idOrAlias: string) =>
|
|
183
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['archive']>>>(
|
|
184
|
+
'POST',
|
|
185
|
+
`agents/${encodeURIComponent(idOrAlias)}/archive`,
|
|
186
|
+
{},
|
|
187
|
+
),
|
|
188
|
+
restore: (idOrAlias: string) =>
|
|
189
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['restore']>>>(
|
|
190
|
+
'POST',
|
|
191
|
+
`agents/${encodeURIComponent(idOrAlias)}/restore`,
|
|
192
|
+
{},
|
|
193
|
+
),
|
|
194
|
+
addAliases: (idOrAlias: string, aliases: string[]) =>
|
|
195
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['addAliases']>>>(
|
|
196
|
+
'POST',
|
|
197
|
+
`agents/${encodeURIComponent(idOrAlias)}/aliases`,
|
|
198
|
+
{ aliases },
|
|
199
|
+
),
|
|
200
|
+
removeAliases: (idOrAlias: string, aliases: string[]) =>
|
|
201
|
+
this.mutation<Awaited<ReturnType<SynomemService['agents']['removeAliases']>>>(
|
|
202
|
+
'POST',
|
|
203
|
+
`agents/${encodeURIComponent(idOrAlias)}/aliases/remove`,
|
|
204
|
+
{ aliases },
|
|
205
|
+
),
|
|
182
206
|
directory: () =>
|
|
183
207
|
this.request<Awaited<ReturnType<SynomemService['agents']['directory']>>>(
|
|
184
208
|
'GET',
|
package/src/schemas.ts
CHANGED
|
@@ -17,12 +17,34 @@ const reservedIds = new Set([
|
|
|
17
17
|
'lpt1',
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* A handle: the human-friendly name for an agent, unique within its workspace.
|
|
22
|
+
*
|
|
23
|
+
* Mutable, unlike the canonical ID. People and agents type this, so it stays
|
|
24
|
+
* lowercase kebab and refuses the reserved words that would collide with
|
|
25
|
+
* filesystem or route segments.
|
|
26
|
+
*/
|
|
27
|
+
export const agentHandleSchema = z
|
|
21
28
|
.string()
|
|
22
29
|
.min(1)
|
|
23
30
|
.max(63)
|
|
24
31
|
.regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase ASCII letters, digits, and hyphens')
|
|
25
|
-
.refine((
|
|
32
|
+
.refine((handle) => !reservedIds.has(handle), 'Reserved agent handle');
|
|
33
|
+
|
|
34
|
+
/** A canonical opaque agent ID: a ULID, uppercase Crockford base32. */
|
|
35
|
+
export const agentUlidSchema = z.string().regex(/^[0-9A-HJKMNP-TV-Z]{26}$/);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* An actor ID as it appears in an event.
|
|
39
|
+
*
|
|
40
|
+
* Accepts a ULID or a handle-shaped name. New agents are created with an opaque
|
|
41
|
+
* ULID so a handle can be renamed without orphaning the events that reference
|
|
42
|
+
* the actor; agents that predate that, and human and system actors, carry a
|
|
43
|
+
* name-shaped ID. Widening rather than replacing keeps append-only history
|
|
44
|
+
* readable — rewriting the actor ID inside stored events to tidy the format
|
|
45
|
+
* would be exactly the rewrite the event log exists to prevent.
|
|
46
|
+
*/
|
|
47
|
+
export const agentIdSchema = z.union([agentUlidSchema, agentHandleSchema]);
|
|
26
48
|
|
|
27
49
|
/**
|
|
28
50
|
* An alias as written, folded to the canonical lowercase form.
|
|
@@ -110,16 +132,42 @@ export const evidenceSchema = z
|
|
|
110
132
|
});
|
|
111
133
|
|
|
112
134
|
export const profileSchema = z.object({
|
|
135
|
+
/** Canonical, opaque and immutable. Events reference this, never the handle. */
|
|
113
136
|
id: agentIdSchema,
|
|
137
|
+
handle: agentHandleSchema,
|
|
114
138
|
displayName: z.string().trim().min(1).max(200),
|
|
115
139
|
aliases: z.array(agentAliasSchema).max(50).optional(),
|
|
116
140
|
description: z.string().trim().max(2000).optional(),
|
|
141
|
+
/** Archived agents keep their history and stop being able to act. */
|
|
142
|
+
status: z.enum(['active', 'archived']).default('active'),
|
|
117
143
|
createdAt: z.string().datetime({ offset: true }),
|
|
118
144
|
metadata: metadataSchema.optional(),
|
|
119
145
|
});
|
|
120
146
|
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Creating an agent names a handle; the canonical ID is generated, never
|
|
149
|
+
* supplied. A caller that could choose the ID could choose one that collides
|
|
150
|
+
* with an archived agent's history.
|
|
151
|
+
*/
|
|
152
|
+
export const createAgentSchema = z
|
|
153
|
+
.object({
|
|
154
|
+
handle: agentHandleSchema,
|
|
155
|
+
displayName: z.string().trim().min(1).max(200),
|
|
156
|
+
aliases: z.array(agentAliasSchema).max(50).optional(),
|
|
157
|
+
description: z.string().trim().max(2000).optional(),
|
|
158
|
+
metadata: metadataSchema.optional(),
|
|
159
|
+
})
|
|
160
|
+
.strict();
|
|
161
|
+
|
|
162
|
+
export const updateAgentSchema = z
|
|
163
|
+
.object({
|
|
164
|
+
handle: agentHandleSchema.optional(),
|
|
165
|
+
displayName: z.string().trim().min(1).max(200).optional(),
|
|
166
|
+
aliases: z.array(agentAliasSchema).max(50).optional(),
|
|
167
|
+
description: z.string().trim().max(2000).optional(),
|
|
168
|
+
metadata: metadataSchema.optional(),
|
|
169
|
+
})
|
|
170
|
+
.strict();
|
|
123
171
|
|
|
124
172
|
/**
|
|
125
173
|
* A runtime binding is a claim about where an agent runs, so the fields stay
|
|
@@ -196,7 +244,14 @@ const agentCreatedSchema = baseEventSchema.extend({
|
|
|
196
244
|
const agentUpdatedSchema = baseEventSchema.extend({
|
|
197
245
|
type: z.literal('agent.updated'),
|
|
198
246
|
agentId: agentIdSchema,
|
|
199
|
-
|
|
247
|
+
/*
|
|
248
|
+
* Archiving is recorded as an update, so `status` belongs in the event even
|
|
249
|
+
* though callers cannot set it through `agents.update` — it moves through
|
|
250
|
+
* `archive` and `restore`, which keep the transition explicit.
|
|
251
|
+
*/
|
|
252
|
+
changes: updateAgentSchema.extend({
|
|
253
|
+
status: z.enum(['active', 'archived']).optional(),
|
|
254
|
+
}),
|
|
200
255
|
});
|
|
201
256
|
|
|
202
257
|
const memoSentSchema = baseEventSchema.extend({
|
package/src/service.ts
CHANGED
|
@@ -79,6 +79,10 @@ export interface SynomemDomainService {
|
|
|
79
79
|
get(idOrAlias: string): Promise<AgentProfile>;
|
|
80
80
|
list(): Promise<AgentProfile[]>;
|
|
81
81
|
resolve(query: string): Promise<AgentResolution>;
|
|
82
|
+
archive(idOrAlias: string): Promise<AgentProfile>;
|
|
83
|
+
restore(idOrAlias: string): Promise<AgentProfile>;
|
|
84
|
+
addAliases(idOrAlias: string, aliases: string[]): Promise<AgentProfile>;
|
|
85
|
+
removeAliases(idOrAlias: string, aliases: string[]): Promise<AgentProfile>;
|
|
82
86
|
directory(): Promise<AgentDirectoryEntry[]>;
|
|
83
87
|
bindings(idOrAlias: string): Promise<AgentRuntimeBinding[]>;
|
|
84
88
|
bindRuntime(input: BindRuntimeInput): Promise<AgentRuntimeBinding>;
|
package/src/storage.ts
CHANGED
|
@@ -255,6 +255,21 @@ CREATE TABLE post_acknowledgments (
|
|
|
255
255
|
CREATE INDEX post_acknowledgments_post ON post_acknowledgments(post_id);
|
|
256
256
|
`;
|
|
257
257
|
|
|
258
|
+
const migrationV7 = `
|
|
259
|
+
-- Opaque canonical IDs, with the handle as a separate mutable name.
|
|
260
|
+
--
|
|
261
|
+
-- Existing agents keep their name-shaped ID and take it as their handle too.
|
|
262
|
+
-- Rewriting the actor ID inside stored events to tidy the format would be
|
|
263
|
+
-- exactly the rewrite an append-only log exists to prevent, so history stays
|
|
264
|
+
-- as written and only NEW agents get a generated opaque ID.
|
|
265
|
+
ALTER TABLE agents ADD COLUMN handle TEXT;
|
|
266
|
+
UPDATE agents SET handle = id WHERE handle IS NULL;
|
|
267
|
+
CREATE UNIQUE INDEX agents_handle ON agents(handle);
|
|
268
|
+
|
|
269
|
+
-- Archived agents keep their records and stop being able to act.
|
|
270
|
+
ALTER TABLE agents ADD COLUMN status TEXT NOT NULL DEFAULT 'active';
|
|
271
|
+
`;
|
|
272
|
+
|
|
258
273
|
const migrationV3 = `
|
|
259
274
|
DROP TRIGGER IF EXISTS events_append_only_update;
|
|
260
275
|
DROP TRIGGER IF EXISTS events_append_only_delete;
|
|
@@ -430,7 +445,8 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
430
445
|
|
|
431
446
|
constructor(options: StorageOptions) {
|
|
432
447
|
this.home = resolve(options.home);
|
|
433
|
-
|
|
448
|
+
// The home is the storage directory; see configLocation in backend.ts.
|
|
449
|
+
this.storageDirectory = this.home;
|
|
434
450
|
this.databasePath = join(this.storageDirectory, 'synomem.sqlite3');
|
|
435
451
|
this.configPath = join(this.storageDirectory, 'config.json');
|
|
436
452
|
this.readOnly = options.readOnly;
|
|
@@ -518,7 +534,7 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
518
534
|
const version = Number(
|
|
519
535
|
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
520
536
|
);
|
|
521
|
-
if (version >
|
|
537
|
+
if (version > 7) {
|
|
522
538
|
throw new SynomemError(
|
|
523
539
|
'UNSUPPORTED_SCHEMA',
|
|
524
540
|
`Database schema version ${version} is newer than this package supports.`,
|
|
@@ -598,14 +614,26 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
598
614
|
db.exec('PRAGMA user_version = 6');
|
|
599
615
|
});
|
|
600
616
|
}
|
|
617
|
+
const afterV6 = Number(
|
|
618
|
+
(db.prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
619
|
+
);
|
|
620
|
+
if (afterV6 === 6) {
|
|
621
|
+
this.transactionSync(() => {
|
|
622
|
+
db.exec(migrationV7);
|
|
623
|
+
db.prepare(
|
|
624
|
+
'INSERT OR IGNORE INTO schema_migrations(version, applied_at) VALUES (?, ?)',
|
|
625
|
+
).run(7, new Date().toISOString());
|
|
626
|
+
db.exec('PRAGMA user_version = 7');
|
|
627
|
+
});
|
|
628
|
+
}
|
|
601
629
|
}
|
|
602
630
|
|
|
603
631
|
private assertSchemaSupported(): void {
|
|
604
632
|
const version = Number(
|
|
605
633
|
(this.db().prepare('PRAGMA user_version').get() as { user_version: number }).user_version,
|
|
606
634
|
);
|
|
607
|
-
if (version !==
|
|
608
|
-
if (version >= 1 && version <=
|
|
635
|
+
if (version !== 7) {
|
|
636
|
+
if (version >= 1 && version <= 6) {
|
|
609
637
|
throw new SynomemError(
|
|
610
638
|
'UNSUPPORTED_SCHEMA',
|
|
611
639
|
`Database schema version ${version} requires migration. Open this home once with readOnly: false, then retry the read-only client.`,
|
|
@@ -1530,12 +1558,23 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1530
1558
|
return { schemaVersion, appliedVersions };
|
|
1531
1559
|
}
|
|
1532
1560
|
|
|
1561
|
+
/**
|
|
1562
|
+
* Aliases that also name an agent directly.
|
|
1563
|
+
*
|
|
1564
|
+
* Matches against the HANDLE as well as the canonical ID. With opaque IDs an
|
|
1565
|
+
* alias can no longer accidentally equal one, but it can easily equal another
|
|
1566
|
+
* agent's handle — which is the collision that actually makes a lookup
|
|
1567
|
+
* ambiguous now.
|
|
1568
|
+
*/
|
|
1533
1569
|
aliasIdentityConflicts(): Array<{ alias: string; agentId: string }> {
|
|
1534
1570
|
return this.db()
|
|
1535
1571
|
.prepare(
|
|
1536
1572
|
`SELECT x.alias, x.agent_id AS agentId
|
|
1537
|
-
FROM aliases x
|
|
1538
|
-
|
|
1573
|
+
FROM aliases x
|
|
1574
|
+
JOIN agents a ON lower(a.id) = x.normalized_alias
|
|
1575
|
+
OR lower(a.handle) = x.normalized_alias
|
|
1576
|
+
WHERE a.id != x.agent_id
|
|
1577
|
+
ORDER BY x.alias`,
|
|
1539
1578
|
)
|
|
1540
1579
|
.all() as unknown as Array<{ alias: string; agentId: string }>;
|
|
1541
1580
|
}
|
|
@@ -1727,10 +1766,13 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1727
1766
|
const parsed = profileSchema.parse(profile);
|
|
1728
1767
|
this.db()
|
|
1729
1768
|
.prepare(
|
|
1730
|
-
|
|
1769
|
+
`INSERT INTO agents(id, handle, status, display_name, profile_json, created_at, updated_at)
|
|
1770
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1731
1771
|
)
|
|
1732
1772
|
.run(
|
|
1733
1773
|
parsed.id,
|
|
1774
|
+
parsed.handle,
|
|
1775
|
+
parsed.status,
|
|
1734
1776
|
parsed.displayName,
|
|
1735
1777
|
JSON.stringify(parsed),
|
|
1736
1778
|
parsed.createdAt,
|
|
@@ -1742,8 +1784,18 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1742
1784
|
updateAgent(profile: AgentProfile, updatedAt: string): void {
|
|
1743
1785
|
const parsed = profileSchema.parse(profile);
|
|
1744
1786
|
this.db()
|
|
1745
|
-
.prepare(
|
|
1746
|
-
|
|
1787
|
+
.prepare(
|
|
1788
|
+
`UPDATE agents SET handle = ?, status = ?, display_name = ?, profile_json = ?,
|
|
1789
|
+
updated_at = ? WHERE id = ?`,
|
|
1790
|
+
)
|
|
1791
|
+
.run(
|
|
1792
|
+
parsed.handle,
|
|
1793
|
+
parsed.status,
|
|
1794
|
+
parsed.displayName,
|
|
1795
|
+
JSON.stringify(parsed),
|
|
1796
|
+
updatedAt,
|
|
1797
|
+
parsed.id,
|
|
1798
|
+
);
|
|
1747
1799
|
this.db().prepare('DELETE FROM aliases WHERE agent_id = ?').run(parsed.id);
|
|
1748
1800
|
this.insertAliases(parsed.id, parsed.aliases ?? []);
|
|
1749
1801
|
}
|
|
@@ -1770,10 +1822,10 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1770
1822
|
`SELECT DISTINCT a.profile_json
|
|
1771
1823
|
FROM agents a
|
|
1772
1824
|
LEFT JOIN aliases x ON x.agent_id = a.id
|
|
1773
|
-
WHERE lower(a.id) = ? OR x.normalized_alias = ?
|
|
1825
|
+
WHERE lower(a.id) = ? OR lower(a.handle) = ? OR x.normalized_alias = ?
|
|
1774
1826
|
ORDER BY a.id ASC`,
|
|
1775
1827
|
)
|
|
1776
|
-
.all(normalized, normalized) as unknown as ProfileRow[];
|
|
1828
|
+
.all(normalized, normalized, normalized) as unknown as ProfileRow[];
|
|
1777
1829
|
const candidates = rows.map((row) => profileSchema.parse(JSON.parse(row.profile_json)));
|
|
1778
1830
|
return candidates.length === 1 ? { match: candidates[0]!, candidates } : { candidates };
|
|
1779
1831
|
}
|
|
@@ -1964,11 +2016,16 @@ export class SynomemStorage implements SynomemRepository {
|
|
|
1964
2016
|
});
|
|
1965
2017
|
}
|
|
1966
2018
|
|
|
1967
|
-
|
|
2019
|
+
/**
|
|
2020
|
+
* @param directory the agent's projection directory name, which is its
|
|
2021
|
+
* handle rather than its canonical ID: these rows are keyed by the path on
|
|
2022
|
+
* disk, and projections are named for people to read.
|
|
2023
|
+
*/
|
|
2024
|
+
replaceAgentProjectionManifest(directory: string, paths: string[], generatedAt: string): void {
|
|
1968
2025
|
this.transactionSync(() => {
|
|
1969
2026
|
this.db()
|
|
1970
2027
|
.prepare('DELETE FROM projection_manifest WHERE path LIKE ? OR path LIKE ?')
|
|
1971
|
-
.run(`${
|
|
2028
|
+
.run(`${directory}/%`, `${directory}\\%`);
|
|
1972
2029
|
const insert = this.db().prepare(
|
|
1973
2030
|
'INSERT INTO projection_manifest(path, generated_at) VALUES (?, ?)',
|
|
1974
2031
|
);
|
package/src/types.ts
CHANGED
|
@@ -28,10 +28,22 @@ export interface EvidenceReference {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export interface AgentProfile {
|
|
31
|
+
/**
|
|
32
|
+
* Canonical, opaque, immutable. Every event references this, so it can never
|
|
33
|
+
* change — which is exactly why the handle exists separately.
|
|
34
|
+
*/
|
|
31
35
|
id: string;
|
|
36
|
+
/** The human-friendly name, unique in the workspace and safe to rename. */
|
|
37
|
+
handle: string;
|
|
32
38
|
displayName: string;
|
|
33
39
|
aliases?: string[];
|
|
34
40
|
description?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Archived agents keep their history and stop being able to act. Events
|
|
43
|
+
* reference the actor permanently, so deletion would leave history pointing
|
|
44
|
+
* at nothing.
|
|
45
|
+
*/
|
|
46
|
+
status: 'active' | 'archived';
|
|
35
47
|
createdAt: string;
|
|
36
48
|
metadata?: Record<string, JsonValue>;
|
|
37
49
|
}
|
|
@@ -684,13 +696,15 @@ export interface BindRuntimeInput {
|
|
|
684
696
|
}
|
|
685
697
|
|
|
686
698
|
export interface CreateAgentInput {
|
|
687
|
-
|
|
699
|
+
/** The handle. The canonical ID is generated, never supplied. */
|
|
700
|
+
handle: string;
|
|
688
701
|
displayName: string;
|
|
689
702
|
aliases?: string[];
|
|
690
703
|
description?: string;
|
|
691
704
|
metadata?: Record<string, JsonValue>;
|
|
692
705
|
}
|
|
693
706
|
export interface UpdateAgentInput {
|
|
707
|
+
handle?: string;
|
|
694
708
|
displayName?: string;
|
|
695
709
|
aliases?: string[];
|
|
696
710
|
description?: string;
|
|
@@ -706,7 +720,15 @@ export interface KudosStats {
|
|
|
706
720
|
byTag: Record<string, number>;
|
|
707
721
|
}
|
|
708
722
|
export interface Diagnostic {
|
|
709
|
-
|
|
723
|
+
/**
|
|
724
|
+
* `skipped` is not a failure.
|
|
725
|
+
*
|
|
726
|
+
* A check the caller lacks permission to run says so and leaves the overall
|
|
727
|
+
* result healthy. Failing the whole diagnostic because an ordinary agent
|
|
728
|
+
* cannot read workspace administration would make `doctor` useless to the
|
|
729
|
+
* callers who need it most.
|
|
730
|
+
*/
|
|
731
|
+
level: 'ok' | 'warning' | 'error' | 'skipped';
|
|
710
732
|
code: string;
|
|
711
733
|
message: string;
|
|
712
734
|
path?: string;
|