@vellumai/assistant 0.10.7-dev.202607101106.c87f4e3 → 0.10.7-dev.202607101252.74ea83d
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/agent/loop-exclusive-tool.test.ts +28 -5
- package/src/agent/loop.ts +4 -14
- package/src/cli/commands/attachment.help.ts +100 -0
- package/src/cli/commands/attachment.ts +92 -157
- package/src/cli/commands/audit.help.ts +40 -0
- package/src/cli/commands/audit.ts +66 -86
- package/src/cli/commands/auth.help.ts +46 -0
- package/src/cli/commands/auth.ts +10 -43
- package/src/cli/index.help.ts +23 -0
- package/src/cli/lib/cli-command-help.ts +80 -0
- package/src/daemon/conversation.ts +21 -4
- package/src/plugin-api/index.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/cli-command-store.test.ts +6 -0
- package/src/plugins/defaults/memory/v2/cli-command-content.ts +52 -0
- package/src/plugins/defaults/memory/v2/cli-command-store.ts +31 -10
- package/src/telemetry/turn-trace-store.test.ts +12 -24
- package/src/telemetry/turn-trace-store.ts +5 -11
package/package.json
CHANGED
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Verifies the agent loop's exclusive-tool dispatch: when a tool the
|
|
3
|
-
*
|
|
2
|
+
* Verifies the agent loop's exclusive-tool dispatch: when a tool the registry
|
|
3
|
+
* marks exclusive appears in a multi-call turn, only that tool runs and the
|
|
4
4
|
* siblings are deferred un-run with a benign result — so the model incorporates
|
|
5
5
|
* the exclusive tool's output before acting on anything else. Drives the REAL
|
|
6
6
|
* loop, mocking only the provider boundary.
|
|
7
7
|
*/
|
|
8
|
-
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { beforeAll, describe, expect, test } from "bun:test";
|
|
9
9
|
|
|
10
10
|
import { createMockProvider } from "../__tests__/helpers/mock-provider.js";
|
|
11
|
+
import { RiskLevel } from "../permissions/types.js";
|
|
11
12
|
import type { ContentBlock, ProviderResponse } from "../providers/types.js";
|
|
13
|
+
import { registerTool } from "../tools/registry.js";
|
|
14
|
+
import type { ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
12
15
|
import { AgentLoop } from "./loop.js";
|
|
13
16
|
|
|
17
|
+
// The loop reads exclusivity straight from the registry (`getTool(name)
|
|
18
|
+
// ?.exclusive`), so seed a registered tool the loop can look up. Other tool
|
|
19
|
+
// names in these turns are absent from the registry, so they read as
|
|
20
|
+
// non-exclusive — exactly the mixed state the deferral logic branches on.
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
registerTool({
|
|
23
|
+
name: "exclusive_tool",
|
|
24
|
+
description: "Exclusive test tool",
|
|
25
|
+
category: "test",
|
|
26
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
27
|
+
executionTarget: "sandbox",
|
|
28
|
+
exclusive: true,
|
|
29
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
30
|
+
async execute(
|
|
31
|
+
_input: Record<string, unknown>,
|
|
32
|
+
_context: ToolContext,
|
|
33
|
+
): Promise<ToolExecutionResult> {
|
|
34
|
+
return { content: "ok", isError: false };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
14
39
|
const endTurn = (text: string): ProviderResponse => ({
|
|
15
40
|
content: [{ type: "text", text }],
|
|
16
41
|
model: "mock-model",
|
|
@@ -82,7 +107,6 @@ describe("AgentLoop — exclusive tool deferral", () => {
|
|
|
82
107
|
executed.push(name);
|
|
83
108
|
return { content: `ran ${name}`, isError: false };
|
|
84
109
|
},
|
|
85
|
-
isExclusiveTool: (name) => name === "exclusive_tool",
|
|
86
110
|
});
|
|
87
111
|
|
|
88
112
|
const { history } = await loop.run({
|
|
@@ -137,7 +161,6 @@ describe("AgentLoop — exclusive tool deferral", () => {
|
|
|
137
161
|
executed.push(name);
|
|
138
162
|
return { content: `ran ${name}`, isError: false };
|
|
139
163
|
},
|
|
140
|
-
isExclusiveTool: (name) => name === "exclusive_tool",
|
|
141
164
|
});
|
|
142
165
|
|
|
143
166
|
const { history } = await loop.run({
|
package/src/agent/loop.ts
CHANGED
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
ToolResultContent,
|
|
44
44
|
} from "../providers/types.js";
|
|
45
45
|
import { isContextOverflowError } from "../providers/types.js";
|
|
46
|
+
import { getTool } from "../tools/registry.js";
|
|
46
47
|
import type { SensitiveOutputBinding } from "../tools/sensitive-output-placeholders.js";
|
|
47
48
|
import {
|
|
48
49
|
applyStreamingSubstitution,
|
|
@@ -708,14 +709,6 @@ export interface AgentLoopConstructorOptions {
|
|
|
708
709
|
tools?: ToolDefinition[];
|
|
709
710
|
toolExecutor?: LoopToolExecutor;
|
|
710
711
|
resolveTools?: (history: Message[]) => ToolDefinition[];
|
|
711
|
-
/**
|
|
712
|
-
* Decide whether a tool runs exclusively in its turn (see
|
|
713
|
-
* {@link ToolDefinition.exclusive}). When it returns true for a tool present
|
|
714
|
-
* in a multi-call turn, the loop runs only that tool and defers the siblings
|
|
715
|
-
* un-run. Injected by the conversation wiring, which can read the tool
|
|
716
|
-
* registry; lightweight loops that omit it never defer.
|
|
717
|
-
*/
|
|
718
|
-
isExclusiveTool?: (toolName: string) => boolean;
|
|
719
712
|
/**
|
|
720
713
|
* Conversation this loop drives. Scopes the loop-held compaction circuit
|
|
721
714
|
* breaker and is the source of truth the loop's pipeline contexts and
|
|
@@ -741,7 +734,6 @@ export class AgentLoop {
|
|
|
741
734
|
private tools: ToolDefinition[];
|
|
742
735
|
private resolveTools: ((history: Message[]) => ToolDefinition[]) | null;
|
|
743
736
|
private toolExecutor: LoopToolExecutor | null;
|
|
744
|
-
private isExclusiveTool: ((toolName: string) => boolean) | null;
|
|
745
737
|
|
|
746
738
|
/**
|
|
747
739
|
* Conversation this loop drives. Source of truth for the `conversationId`
|
|
@@ -771,7 +763,6 @@ export class AgentLoop {
|
|
|
771
763
|
tools,
|
|
772
764
|
toolExecutor,
|
|
773
765
|
resolveTools,
|
|
774
|
-
isExclusiveTool,
|
|
775
766
|
conversationId,
|
|
776
767
|
resolveConversationDir,
|
|
777
768
|
} = options;
|
|
@@ -781,7 +772,6 @@ export class AgentLoop {
|
|
|
781
772
|
this.tools = tools ?? [];
|
|
782
773
|
this.resolveTools = resolveTools ?? null;
|
|
783
774
|
this.toolExecutor = toolExecutor ?? null;
|
|
784
|
-
this.isExclusiveTool = isExclusiveTool ?? null;
|
|
785
775
|
this.conversationId = conversationId;
|
|
786
776
|
this.resolveConversationDir = resolveConversationDir ?? null;
|
|
787
777
|
this.compactionCircuit = new CompactionCircuit(this.conversationId);
|
|
@@ -2069,9 +2059,9 @@ export class AgentLoop {
|
|
|
2069
2059
|
// the siblings with a benign, un-run result so the model re-issues them
|
|
2070
2060
|
// next turn if still needed. Every tool_use still gets a matching
|
|
2071
2061
|
// tool_result, so history stays well-formed.
|
|
2072
|
-
const exclusiveBlock =
|
|
2073
|
-
|
|
2074
|
-
|
|
2062
|
+
const exclusiveBlock = toolUseBlocks.find(
|
|
2063
|
+
(block) => getTool(block.name)?.exclusive === true,
|
|
2064
|
+
);
|
|
2075
2065
|
const deferSiblings =
|
|
2076
2066
|
exclusiveBlock !== undefined && toolUseBlocks.length > 1;
|
|
2077
2067
|
if (deferSiblings) {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative help for the `assistant attachment` command.
|
|
3
|
+
*
|
|
4
|
+
* Plain data (no action handlers, imports only the help contract type) so the
|
|
5
|
+
* memory capability indexer can read it without pulling in the daemon/IPC action
|
|
6
|
+
* graph. The handlers live in `attachment.ts`, which applies this via
|
|
7
|
+
* `applyCommandHelp` and attaches them.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CliCommandHelp } from "../lib/cli-command-help.js";
|
|
11
|
+
|
|
12
|
+
export const attachmentHelp: CliCommandHelp = {
|
|
13
|
+
name: "attachment",
|
|
14
|
+
description: "Manage file attachments for conversations",
|
|
15
|
+
helpText: `
|
|
16
|
+
Attachments come in two flavours:
|
|
17
|
+
|
|
18
|
+
File-backed Large files stored by path reference (no memory copy).
|
|
19
|
+
The file must remain on disk for the lifetime of the
|
|
20
|
+
attachment.
|
|
21
|
+
Inline Small payloads encoded directly (handled internally).
|
|
22
|
+
|
|
23
|
+
Use 'register' to record a file-backed attachment and 'lookup' to
|
|
24
|
+
retrieve its stored path by the original source location.
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
$ assistant attachment register --path /tmp/clip.mp4 --mime video/mp4
|
|
28
|
+
$ assistant attachment register --path /tmp/clip.mp4 --mime video/mp4 --filename recording.mp4
|
|
29
|
+
$ assistant attachment lookup --source /tmp/clip.mp4 --conversation conv_abc123`,
|
|
30
|
+
subcommands: [
|
|
31
|
+
{
|
|
32
|
+
name: "register",
|
|
33
|
+
description: "Register a file-backed attachment with the assistant",
|
|
34
|
+
options: [
|
|
35
|
+
{
|
|
36
|
+
flags: "--path <file>",
|
|
37
|
+
description: "Absolute path to the file (required)",
|
|
38
|
+
required: true,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
flags: "--mime <type>",
|
|
42
|
+
description: "MIME type of the file (required)",
|
|
43
|
+
required: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
flags: "--filename <name>",
|
|
47
|
+
description: "Display filename (defaults to basename of path)",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
flags: "--json",
|
|
51
|
+
description: "Output result as machine-readable JSON.",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
helpText: `
|
|
55
|
+
Registers a file on disk as a file-backed attachment in the assistant's
|
|
56
|
+
attachment store. The file must exist at the given path and must remain
|
|
57
|
+
on disk for the lifetime of the attachment — the assistant stores a
|
|
58
|
+
path reference, not a copy.
|
|
59
|
+
|
|
60
|
+
Returns the attachment ID and metadata on success.
|
|
61
|
+
|
|
62
|
+
Examples:
|
|
63
|
+
$ assistant attachment register --path /tmp/clip.mp4 --mime video/mp4
|
|
64
|
+
$ assistant attachment register --path /tmp/screen.png --mime image/png --filename screenshot.png
|
|
65
|
+
$ assistant attachment register --path /tmp/audio.wav --mime audio/wav --json`,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "lookup",
|
|
69
|
+
description: "Look up a stored attachment by its original source path",
|
|
70
|
+
options: [
|
|
71
|
+
{
|
|
72
|
+
flags: "--source <path>",
|
|
73
|
+
description: "Original source path of the file (required)",
|
|
74
|
+
required: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
flags: "--conversation <id>",
|
|
78
|
+
description:
|
|
79
|
+
"Conversation ID to search within (required) — run 'assistant conversations list' to find it",
|
|
80
|
+
required: true,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
flags: "--json",
|
|
84
|
+
description: "Output result as machine-readable JSON.",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
helpText: `
|
|
88
|
+
Searches for an attachment that was previously registered with the
|
|
89
|
+
given source path, scoped to a specific conversation. Returns the
|
|
90
|
+
stored file path on success.
|
|
91
|
+
|
|
92
|
+
Attachments are linked to messages within conversations. Use
|
|
93
|
+
'assistant conversations list' to find the conversation ID.
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
$ assistant attachment lookup --source /tmp/clip.mp4 --conversation conv_abc123
|
|
97
|
+
$ assistant attachment lookup --source /path/to/recording.mp4 --conversation conv_xyz --json`,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
@@ -3,189 +3,124 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Subcommands: register, lookup — thin wrappers over the daemon's
|
|
5
5
|
* attachment routes (`attachment_register`, `attachment_lookup`).
|
|
6
|
+
*
|
|
7
|
+
* The command's help structure lives in `attachment.help.ts` (import-safe for
|
|
8
|
+
* the memory capability indexer); this module applies it and attaches the
|
|
9
|
+
* action handlers.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import type { Command } from "commander";
|
|
9
13
|
|
|
10
14
|
import { cliIpcCall } from "../../ipc/cli-client.js";
|
|
15
|
+
import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
|
|
11
16
|
import { registerCommand } from "../lib/register-command.js";
|
|
12
17
|
import { log } from "../logger.js";
|
|
13
18
|
import { shouldOutputJson, writeOutput } from "../output.js";
|
|
19
|
+
import { attachmentHelp } from "./attachment.help.js";
|
|
14
20
|
|
|
15
21
|
// ── Registration ──────────────────────────────────────────────────────
|
|
16
22
|
|
|
17
23
|
export function registerAttachmentCommand(program: Command): void {
|
|
18
24
|
registerCommand(program, {
|
|
19
|
-
name:
|
|
25
|
+
name: attachmentHelp.name,
|
|
20
26
|
transport: "ipc",
|
|
21
|
-
description:
|
|
27
|
+
description: attachmentHelp.description,
|
|
22
28
|
build: (attachment) => {
|
|
29
|
+
applyCommandHelp(attachment, attachmentHelp);
|
|
30
|
+
|
|
31
|
+
// ── register ───────────────────────────────────────────────────
|
|
32
|
+
subcommand(attachment, "register").action(
|
|
33
|
+
async (
|
|
34
|
+
opts: {
|
|
35
|
+
path: string;
|
|
36
|
+
mime: string;
|
|
37
|
+
filename?: string;
|
|
38
|
+
json?: boolean;
|
|
39
|
+
},
|
|
40
|
+
cmd: Command,
|
|
41
|
+
) => {
|
|
42
|
+
const jsonOutput = opts.json || shouldOutputJson(cmd);
|
|
43
|
+
|
|
44
|
+
const result = await cliIpcCall<{
|
|
45
|
+
id: string;
|
|
46
|
+
originalFilename: string;
|
|
47
|
+
mimeType: string;
|
|
48
|
+
sizeBytes: number;
|
|
49
|
+
kind: string;
|
|
50
|
+
filePath: string;
|
|
51
|
+
createdAt: number;
|
|
52
|
+
}>("attachment_register", {
|
|
53
|
+
body: {
|
|
54
|
+
path: opts.path,
|
|
55
|
+
mimeType: opts.mime,
|
|
56
|
+
filename: opts.filename,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
23
59
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Use 'register' to record a file-backed attachment and 'lookup' to
|
|
35
|
-
retrieve its stored path by the original source location.
|
|
36
|
-
|
|
37
|
-
Examples:
|
|
38
|
-
$ assistant attachment register --path /tmp/clip.mp4 --mime video/mp4
|
|
39
|
-
$ assistant attachment register --path /tmp/clip.mp4 --mime video/mp4 --filename recording.mp4
|
|
40
|
-
$ assistant attachment lookup --source /tmp/clip.mp4 --conversation conv_abc123`,
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
// ── register ─────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
attachment
|
|
46
|
-
.command("register")
|
|
47
|
-
.description("Register a file-backed attachment with the assistant")
|
|
48
|
-
.requiredOption("--path <file>", "Absolute path to the file (required)")
|
|
49
|
-
.requiredOption("--mime <type>", "MIME type of the file (required)")
|
|
50
|
-
.option(
|
|
51
|
-
"--filename <name>",
|
|
52
|
-
"Display filename (defaults to basename of path)",
|
|
53
|
-
)
|
|
54
|
-
.option("--json", "Output result as machine-readable JSON.")
|
|
55
|
-
.addHelpText(
|
|
56
|
-
"after",
|
|
57
|
-
`
|
|
58
|
-
Registers a file on disk as a file-backed attachment in the assistant's
|
|
59
|
-
attachment store. The file must exist at the given path and must remain
|
|
60
|
-
on disk for the lifetime of the attachment — the assistant stores a
|
|
61
|
-
path reference, not a copy.
|
|
62
|
-
|
|
63
|
-
Returns the attachment ID and metadata on success.
|
|
64
|
-
|
|
65
|
-
Examples:
|
|
66
|
-
$ assistant attachment register --path /tmp/clip.mp4 --mime video/mp4
|
|
67
|
-
$ assistant attachment register --path /tmp/screen.png --mime image/png --filename screenshot.png
|
|
68
|
-
$ assistant attachment register --path /tmp/audio.wav --mime audio/wav --json`,
|
|
69
|
-
)
|
|
70
|
-
.action(
|
|
71
|
-
async (
|
|
72
|
-
opts: {
|
|
73
|
-
path: string;
|
|
74
|
-
mime: string;
|
|
75
|
-
filename?: string;
|
|
76
|
-
json?: boolean;
|
|
77
|
-
},
|
|
78
|
-
cmd: Command,
|
|
79
|
-
) => {
|
|
80
|
-
const jsonOutput = opts.json || shouldOutputJson(cmd);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
if (jsonOutput) {
|
|
62
|
+
writeOutput(cmd, { ok: false, error: result.error });
|
|
63
|
+
} else {
|
|
64
|
+
log.error(result.error ?? "Unknown error");
|
|
65
|
+
}
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
81
69
|
|
|
82
|
-
|
|
83
|
-
id: string;
|
|
84
|
-
originalFilename: string;
|
|
85
|
-
mimeType: string;
|
|
86
|
-
sizeBytes: number;
|
|
87
|
-
kind: string;
|
|
88
|
-
filePath: string;
|
|
89
|
-
createdAt: number;
|
|
90
|
-
}>("attachment_register", {
|
|
91
|
-
body: {
|
|
92
|
-
path: opts.path,
|
|
93
|
-
mimeType: opts.mime,
|
|
94
|
-
filename: opts.filename,
|
|
95
|
-
},
|
|
96
|
-
});
|
|
70
|
+
const record = result.result!;
|
|
97
71
|
|
|
98
|
-
if (!result.ok) {
|
|
99
72
|
if (jsonOutput) {
|
|
100
|
-
writeOutput(cmd, { ok:
|
|
73
|
+
writeOutput(cmd, { ok: true, ...record });
|
|
101
74
|
} else {
|
|
102
|
-
|
|
75
|
+
process.stdout.write(`${record.id}\n`);
|
|
76
|
+
log.info(`Attachment registered: ${record.id}`);
|
|
77
|
+
log.info(` Filename: ${record.originalFilename}`);
|
|
78
|
+
log.info(` MIME: ${record.mimeType}`);
|
|
79
|
+
log.info(` Size: ${record.sizeBytes} bytes`);
|
|
80
|
+
log.info(` Kind: ${record.kind}`);
|
|
81
|
+
log.info(` Path: ${record.filePath}`);
|
|
103
82
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
// ── lookup ───────────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
attachment
|
|
127
|
-
.command("lookup")
|
|
128
|
-
.description("Look up a stored attachment by its original source path")
|
|
129
|
-
.requiredOption(
|
|
130
|
-
"--source <path>",
|
|
131
|
-
"Original source path of the file (required)",
|
|
132
|
-
)
|
|
133
|
-
.requiredOption(
|
|
134
|
-
"--conversation <id>",
|
|
135
|
-
"Conversation ID to search within (required) — run 'assistant conversations list' to find it",
|
|
136
|
-
)
|
|
137
|
-
.option("--json", "Output result as machine-readable JSON.")
|
|
138
|
-
.addHelpText(
|
|
139
|
-
"after",
|
|
140
|
-
`
|
|
141
|
-
Searches for an attachment that was previously registered with the
|
|
142
|
-
given source path, scoped to a specific conversation. Returns the
|
|
143
|
-
stored file path on success.
|
|
144
|
-
|
|
145
|
-
Attachments are linked to messages within conversations. Use
|
|
146
|
-
'assistant conversations list' to find the conversation ID.
|
|
147
|
-
|
|
148
|
-
Examples:
|
|
149
|
-
$ assistant attachment lookup --source /tmp/clip.mp4 --conversation conv_abc123
|
|
150
|
-
$ assistant attachment lookup --source /path/to/recording.mp4 --conversation conv_xyz --json`,
|
|
151
|
-
)
|
|
152
|
-
.action(
|
|
153
|
-
async (
|
|
154
|
-
opts: { source: string; conversation: string; json?: boolean },
|
|
155
|
-
cmd: Command,
|
|
156
|
-
) => {
|
|
157
|
-
const jsonOutput = opts.json || shouldOutputJson(cmd);
|
|
158
|
-
|
|
159
|
-
const result = await cliIpcCall<{ filePath: string }>(
|
|
160
|
-
"attachment_lookup",
|
|
161
|
-
{
|
|
162
|
-
body: {
|
|
163
|
-
sourcePath: opts.source,
|
|
164
|
-
conversationId: opts.conversation,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// ── lookup ─────────────────────────────────────────────────────
|
|
87
|
+
subcommand(attachment, "lookup").action(
|
|
88
|
+
async (
|
|
89
|
+
opts: { source: string; conversation: string; json?: boolean },
|
|
90
|
+
cmd: Command,
|
|
91
|
+
) => {
|
|
92
|
+
const jsonOutput = opts.json || shouldOutputJson(cmd);
|
|
93
|
+
|
|
94
|
+
const result = await cliIpcCall<{ filePath: string }>(
|
|
95
|
+
"attachment_lookup",
|
|
96
|
+
{
|
|
97
|
+
body: {
|
|
98
|
+
sourcePath: opts.source,
|
|
99
|
+
conversationId: opts.conversation,
|
|
100
|
+
},
|
|
165
101
|
},
|
|
166
|
-
|
|
167
|
-
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!result.ok) {
|
|
105
|
+
if (jsonOutput) {
|
|
106
|
+
writeOutput(cmd, { ok: false, error: result.error });
|
|
107
|
+
} else {
|
|
108
|
+
log.error(result.error ?? "Unknown error");
|
|
109
|
+
}
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
168
113
|
|
|
169
|
-
if (!result.ok) {
|
|
170
114
|
if (jsonOutput) {
|
|
171
|
-
writeOutput(cmd, {
|
|
115
|
+
writeOutput(cmd, {
|
|
116
|
+
ok: true,
|
|
117
|
+
filePath: result.result!.filePath,
|
|
118
|
+
});
|
|
172
119
|
} else {
|
|
173
|
-
|
|
120
|
+
process.stdout.write(result.result!.filePath + "\n");
|
|
174
121
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (jsonOutput) {
|
|
180
|
-
writeOutput(cmd, {
|
|
181
|
-
ok: true,
|
|
182
|
-
filePath: result.result!.filePath,
|
|
183
|
-
});
|
|
184
|
-
} else {
|
|
185
|
-
process.stdout.write(result.result!.filePath + "\n");
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
);
|
|
122
|
+
},
|
|
123
|
+
);
|
|
189
124
|
},
|
|
190
125
|
});
|
|
191
126
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative help for the `assistant audit` command.
|
|
3
|
+
*
|
|
4
|
+
* Plain data (no action handlers, imports only the help contract type) so the
|
|
5
|
+
* memory capability indexer can read it without pulling in the daemon/IPC action
|
|
6
|
+
* graph. The handler lives in `audit.ts`, which applies this via
|
|
7
|
+
* `applyCommandHelp` and attaches it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CliCommandHelp } from "../lib/cli-command-help.js";
|
|
11
|
+
|
|
12
|
+
export const auditHelp: CliCommandHelp = {
|
|
13
|
+
name: "audit",
|
|
14
|
+
description: "Show recent tool invocations",
|
|
15
|
+
options: [
|
|
16
|
+
{
|
|
17
|
+
flags: "-l, --limit <n>",
|
|
18
|
+
description: "Number of entries to show",
|
|
19
|
+
defaultValue: "20",
|
|
20
|
+
},
|
|
21
|
+
{ flags: "--json", description: "Output raw JSON" },
|
|
22
|
+
],
|
|
23
|
+
helpText: `
|
|
24
|
+
Reads from the tool invocation audit log via the daemon. Each row
|
|
25
|
+
represents one tool call the assistant made, including what was invoked,
|
|
26
|
+
how the approval system classified it, and how long it took.
|
|
27
|
+
|
|
28
|
+
Table columns:
|
|
29
|
+
Timestamp When the tool was invoked (UTC, YYYY-MM-DD HH:MM:SS)
|
|
30
|
+
Tool Tool name (e.g. bash, read_file, write_file, browser)
|
|
31
|
+
Input Truncated summary of the tool input (command, path, etc.)
|
|
32
|
+
Decision Approval decision: allow, deny, or ask
|
|
33
|
+
Risk Risk classification: none, low, medium, high
|
|
34
|
+
Duration Wall-clock execution time (e.g. 120ms, 1.3s)
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
$ assistant audit
|
|
38
|
+
$ assistant audit --limit 50
|
|
39
|
+
$ assistant audit --json`,
|
|
40
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
|
|
3
3
|
import { cliIpcCall, exitFromIpcResult } from "../../ipc/cli-client.js";
|
|
4
|
+
import { applyCommandHelp } from "../lib/cli-command-help.js";
|
|
4
5
|
import { registerCommand } from "../lib/register-command.js";
|
|
5
6
|
import { log } from "../logger.js";
|
|
7
|
+
import { auditHelp } from "./audit.help.js";
|
|
6
8
|
|
|
7
9
|
interface ToolInvocationRow {
|
|
8
10
|
toolName: string;
|
|
@@ -15,100 +17,78 @@ interface ToolInvocationRow {
|
|
|
15
17
|
|
|
16
18
|
export function registerAuditCommand(program: Command): void {
|
|
17
19
|
registerCommand(program, {
|
|
18
|
-
name:
|
|
20
|
+
name: auditHelp.name,
|
|
19
21
|
transport: "ipc",
|
|
20
|
-
description:
|
|
22
|
+
description: auditHelp.description,
|
|
21
23
|
build: (audit) => {
|
|
22
|
-
audit
|
|
23
|
-
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
Tool Tool name (e.g. bash, read_file, write_file, browser)
|
|
35
|
-
Input Truncated summary of the tool input (command, path, etc.)
|
|
36
|
-
Decision Approval decision: allow, deny, or ask
|
|
37
|
-
Risk Risk classification: none, low, medium, high
|
|
38
|
-
Duration Wall-clock execution time (e.g. 120ms, 1.3s)
|
|
24
|
+
applyCommandHelp(audit, auditHelp);
|
|
25
|
+
audit.action(async (opts: { limit: string; json?: boolean }) => {
|
|
26
|
+
const limit = parseInt(opts.limit, 10) || 20;
|
|
27
|
+
const response = await cliIpcCall<{
|
|
28
|
+
invocations: ToolInvocationRow[];
|
|
29
|
+
}>("audit_list", {
|
|
30
|
+
queryParams: { limit: String(limit) },
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
return exitFromIpcResult(response);
|
|
34
|
+
}
|
|
35
|
+
const rows = response.result!.invocations;
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
)
|
|
45
|
-
.action(async (opts: { limit: string; json?: boolean }) => {
|
|
46
|
-
const limit = parseInt(opts.limit, 10) || 20;
|
|
47
|
-
const response = await cliIpcCall<{
|
|
48
|
-
invocations: ToolInvocationRow[];
|
|
49
|
-
}>("audit_list", {
|
|
50
|
-
queryParams: { limit: String(limit) },
|
|
51
|
-
});
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
return exitFromIpcResult(response);
|
|
54
|
-
}
|
|
55
|
-
const rows = response.result!.invocations;
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
console.log(JSON.stringify(rows, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
56
41
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
if (rows.length === 0) {
|
|
43
|
+
log.info("No tool invocations recorded");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const tsW = 20;
|
|
47
|
+
const toolW = 14;
|
|
48
|
+
const inputW = 30;
|
|
49
|
+
const decW = 8;
|
|
50
|
+
const riskW = 8;
|
|
51
|
+
const durW = 8;
|
|
52
|
+
log.info(
|
|
53
|
+
"Timestamp".padEnd(tsW) +
|
|
54
|
+
"Tool".padEnd(toolW) +
|
|
55
|
+
"Input".padEnd(inputW) +
|
|
56
|
+
"Decision".padEnd(decW) +
|
|
57
|
+
"Risk".padEnd(riskW) +
|
|
58
|
+
"Duration",
|
|
59
|
+
);
|
|
60
|
+
log.info("-".repeat(tsW + toolW + inputW + decW + riskW + durW));
|
|
61
|
+
for (const r of rows) {
|
|
62
|
+
const ts = new Date(r.createdAt)
|
|
63
|
+
.toISOString()
|
|
64
|
+
.slice(0, 19)
|
|
65
|
+
.replace("T", " ");
|
|
66
|
+
let inputSummary = "";
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(r.input);
|
|
69
|
+
if (parsed.command) inputSummary = parsed.command;
|
|
70
|
+
else if (parsed.path) inputSummary = parsed.path;
|
|
71
|
+
else inputSummary = r.input;
|
|
72
|
+
} catch {
|
|
73
|
+
inputSummary = r.input;
|
|
60
74
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
log.info("No tool invocations recorded");
|
|
64
|
-
return;
|
|
75
|
+
if (inputSummary.length > inputW - 2) {
|
|
76
|
+
inputSummary = inputSummary.slice(0, inputW - 4) + "..";
|
|
65
77
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const riskW = 8;
|
|
71
|
-
const durW = 8;
|
|
78
|
+
const dur =
|
|
79
|
+
r.durationMs < 1000
|
|
80
|
+
? `${r.durationMs}ms`
|
|
81
|
+
: `${(r.durationMs / 1000).toFixed(1)}s`;
|
|
72
82
|
log.info(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
ts.padEnd(tsW) +
|
|
84
|
+
r.toolName.padEnd(toolW) +
|
|
85
|
+
inputSummary.padEnd(inputW) +
|
|
86
|
+
r.decision.padEnd(decW) +
|
|
87
|
+
r.riskLevel.padEnd(riskW) +
|
|
88
|
+
dur,
|
|
79
89
|
);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const ts = new Date(r.createdAt)
|
|
83
|
-
.toISOString()
|
|
84
|
-
.slice(0, 19)
|
|
85
|
-
.replace("T", " ");
|
|
86
|
-
let inputSummary = "";
|
|
87
|
-
try {
|
|
88
|
-
const parsed = JSON.parse(r.input);
|
|
89
|
-
if (parsed.command) inputSummary = parsed.command;
|
|
90
|
-
else if (parsed.path) inputSummary = parsed.path;
|
|
91
|
-
else inputSummary = r.input;
|
|
92
|
-
} catch {
|
|
93
|
-
inputSummary = r.input;
|
|
94
|
-
}
|
|
95
|
-
if (inputSummary.length > inputW - 2) {
|
|
96
|
-
inputSummary = inputSummary.slice(0, inputW - 4) + "..";
|
|
97
|
-
}
|
|
98
|
-
const dur =
|
|
99
|
-
r.durationMs < 1000
|
|
100
|
-
? `${r.durationMs}ms`
|
|
101
|
-
: `${(r.durationMs / 1000).toFixed(1)}s`;
|
|
102
|
-
log.info(
|
|
103
|
-
ts.padEnd(tsW) +
|
|
104
|
-
r.toolName.padEnd(toolW) +
|
|
105
|
-
inputSummary.padEnd(inputW) +
|
|
106
|
-
r.decision.padEnd(decW) +
|
|
107
|
-
r.riskLevel.padEnd(riskW) +
|
|
108
|
-
dur,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
112
92
|
},
|
|
113
93
|
});
|
|
114
94
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative help for the `assistant auth` command.
|
|
3
|
+
*
|
|
4
|
+
* Plain data (no action handlers, imports only the help contract type) so the
|
|
5
|
+
* memory capability indexer can read it without pulling in the daemon/IPC action
|
|
6
|
+
* graph. The handlers live in `auth.ts`, which applies this via `applyCommandHelp`
|
|
7
|
+
* and attaches them.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CliCommandHelp } from "../lib/cli-command-help.js";
|
|
11
|
+
|
|
12
|
+
export const authHelp: CliCommandHelp = {
|
|
13
|
+
name: "auth",
|
|
14
|
+
description: "Manage platform authentication and identity",
|
|
15
|
+
options: [
|
|
16
|
+
{ flags: "--json", description: "Machine-readable compact JSON output" },
|
|
17
|
+
],
|
|
18
|
+
helpText: `
|
|
19
|
+
The auth namespace manages the assistant's authentication state with the
|
|
20
|
+
Vellum platform. It provides commands to inspect identity and connection
|
|
21
|
+
status, helping diagnose configuration issues.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
$ assistant auth info
|
|
25
|
+
$ assistant auth info --json`,
|
|
26
|
+
subcommands: [
|
|
27
|
+
{
|
|
28
|
+
name: "info",
|
|
29
|
+
description: "Show platform identity and authentication status",
|
|
30
|
+
helpText: `
|
|
31
|
+
Fields:
|
|
32
|
+
platformUrl The Vellum platform base URL this assistant connects to
|
|
33
|
+
assistantId This assistant's platform UUID
|
|
34
|
+
organizationId The organization this assistant belongs to (from PLATFORM_ORGANIZATION_ID)
|
|
35
|
+
userId The user who owns this assistant (from PLATFORM_USER_ID)
|
|
36
|
+
authenticated Whether all prerequisites for platform authentication are met
|
|
37
|
+
(platform URL and assistant API key both present)
|
|
38
|
+
|
|
39
|
+
When not authenticated, a message field provides guidance on next steps.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
$ assistant auth info
|
|
43
|
+
$ assistant auth info --json`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
package/src/cli/commands/auth.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
|
|
3
3
|
import { cliIpcCall, exitFromIpcResult } from "../../ipc/cli-client.js";
|
|
4
|
+
import { applyCommandHelp, subcommand } from "../lib/cli-command-help.js";
|
|
4
5
|
import { registerCommand } from "../lib/register-command.js";
|
|
5
6
|
import { log } from "../logger.js";
|
|
6
7
|
import { shouldOutputJson, writeOutput } from "../output.js";
|
|
8
|
+
import { authHelp } from "./auth.help.js";
|
|
7
9
|
|
|
8
10
|
interface AuthInfoResponse {
|
|
9
11
|
platformUrl: string | null;
|
|
@@ -16,51 +18,15 @@ interface AuthInfoResponse {
|
|
|
16
18
|
|
|
17
19
|
export function registerAuthCommand(program: Command): void {
|
|
18
20
|
registerCommand(program, {
|
|
19
|
-
name:
|
|
21
|
+
name: authHelp.name,
|
|
20
22
|
transport: "ipc",
|
|
21
|
-
description:
|
|
23
|
+
description: authHelp.description,
|
|
22
24
|
build: (auth) => {
|
|
23
|
-
auth
|
|
25
|
+
applyCommandHelp(auth, authHelp);
|
|
24
26
|
|
|
25
|
-
auth.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
The auth namespace manages the assistant's authentication state with the
|
|
29
|
-
Vellum platform. It provides commands to inspect identity and connection
|
|
30
|
-
status, helping diagnose configuration issues.
|
|
31
|
-
|
|
32
|
-
Examples:
|
|
33
|
-
$ assistant auth info
|
|
34
|
-
$ assistant auth info --json`,
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
// -----------------------------------------------------------------------
|
|
38
|
-
// info
|
|
39
|
-
// -----------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
auth
|
|
42
|
-
.command("info")
|
|
43
|
-
.description("Show platform identity and authentication status")
|
|
44
|
-
.addHelpText(
|
|
45
|
-
"after",
|
|
46
|
-
`
|
|
47
|
-
Fields:
|
|
48
|
-
platformUrl The Vellum platform base URL this assistant connects to
|
|
49
|
-
assistantId This assistant's platform UUID
|
|
50
|
-
organizationId The organization this assistant belongs to (from PLATFORM_ORGANIZATION_ID)
|
|
51
|
-
userId The user who owns this assistant (from PLATFORM_USER_ID)
|
|
52
|
-
authenticated Whether all prerequisites for platform authentication are met
|
|
53
|
-
(platform URL and assistant API key both present)
|
|
54
|
-
|
|
55
|
-
When not authenticated, a message field provides guidance on next steps.
|
|
56
|
-
|
|
57
|
-
Examples:
|
|
58
|
-
$ assistant auth info
|
|
59
|
-
$ assistant auth info --json`,
|
|
60
|
-
)
|
|
61
|
-
.action(async (_opts: Record<string, unknown>, cmd: Command) => {
|
|
62
|
-
const response =
|
|
63
|
-
await cliIpcCall<AuthInfoResponse>("auth_info");
|
|
27
|
+
subcommand(auth, "info").action(
|
|
28
|
+
async (_opts: Record<string, unknown>, cmd: Command) => {
|
|
29
|
+
const response = await cliIpcCall<AuthInfoResponse>("auth_info");
|
|
64
30
|
|
|
65
31
|
if (!response.ok) {
|
|
66
32
|
return exitFromIpcResult(response);
|
|
@@ -89,7 +55,8 @@ Examples:
|
|
|
89
55
|
log.info(result.message);
|
|
90
56
|
}
|
|
91
57
|
}
|
|
92
|
-
}
|
|
58
|
+
},
|
|
59
|
+
);
|
|
93
60
|
},
|
|
94
61
|
});
|
|
95
62
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Aggregated declarative help for the top-level `assistant` CLI commands that
|
|
3
|
+
* have adopted the static-help split (`<command>.help.ts`).
|
|
4
|
+
*
|
|
5
|
+
* Plugins — notably the memory capability indexer — import this to read command
|
|
6
|
+
* help without importing `cli/program.ts`, which pulls every command's action
|
|
7
|
+
* handler (and its daemon/IPC deps) into the import graph. Entries are pure data
|
|
8
|
+
* (see {@link ./lib/cli-command-help.CliCommandHelp}); consumers iterate them.
|
|
9
|
+
*
|
|
10
|
+
* Extend this as commands adopt the split. Once every command is listed here,
|
|
11
|
+
* the memory indexer no longer needs `buildCliProgramTree()`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { attachmentHelp } from "./commands/attachment.help.js";
|
|
15
|
+
import { auditHelp } from "./commands/audit.help.js";
|
|
16
|
+
import { authHelp } from "./commands/auth.help.js";
|
|
17
|
+
import type { CliCommandHelp } from "./lib/cli-command-help.js";
|
|
18
|
+
|
|
19
|
+
export const CLI_COMMAND_HELP: readonly CliCommandHelp[] = [
|
|
20
|
+
attachmentHelp,
|
|
21
|
+
auditHelp,
|
|
22
|
+
authHelp,
|
|
23
|
+
];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fully declarative description of a top-level `assistant` CLI command and its
|
|
5
|
+
* subcommands. Plain data by design — no action handlers — so plugins (e.g. the
|
|
6
|
+
* memory capability indexer) can import a command's help and iterate over it
|
|
7
|
+
* without dragging in the CLI's daemon/IPC action graph. Command modules apply
|
|
8
|
+
* the same data via {@link applyCommandHelp}, then attach their handlers.
|
|
9
|
+
*/
|
|
10
|
+
export interface CliCommandHelp {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
/** Options declared directly on the top-level command. */
|
|
14
|
+
options?: CliOptionHelp[];
|
|
15
|
+
/** Extra help appended after the option list (`addHelpText("after", …)`). */
|
|
16
|
+
helpText?: string;
|
|
17
|
+
subcommands?: CliSubcommandHelp[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CliSubcommandHelp {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
options?: CliOptionHelp[];
|
|
24
|
+
/** Extra help appended after the option list (`addHelpText("after", …)`). */
|
|
25
|
+
helpText?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CliOptionHelp {
|
|
29
|
+
/** Commander flag spec, e.g. `"--path <file>"` or `"-l, --limit <n>"`. */
|
|
30
|
+
flags: string;
|
|
31
|
+
description: string;
|
|
32
|
+
/** When true, applied via `requiredOption` (missing → error) rather than `option`. */
|
|
33
|
+
required?: boolean;
|
|
34
|
+
/** Default value passed to `option(flags, description, defaultValue)`. */
|
|
35
|
+
defaultValue?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applyOptions(command: Command, options?: CliOptionHelp[]): void {
|
|
39
|
+
for (const option of options ?? []) {
|
|
40
|
+
if (option.required) {
|
|
41
|
+
command.requiredOption(option.flags, option.description);
|
|
42
|
+
} else if (option.defaultValue !== undefined) {
|
|
43
|
+
command.option(option.flags, option.description, option.defaultValue);
|
|
44
|
+
} else {
|
|
45
|
+
command.option(option.flags, option.description);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Configure a Commander command from its declarative {@link CliCommandHelp}:
|
|
52
|
+
* top-level options, appended help text, and subcommands (with their options).
|
|
53
|
+
* Does not set the top-level name/description — `registerCommand` owns those —
|
|
54
|
+
* and does not attach action handlers; the command module attaches those to the
|
|
55
|
+
* command or its subcommands.
|
|
56
|
+
*/
|
|
57
|
+
export function applyCommandHelp(command: Command, help: CliCommandHelp): void {
|
|
58
|
+
applyOptions(command, help.options);
|
|
59
|
+
if (help.helpText) {
|
|
60
|
+
command.addHelpText("after", help.helpText);
|
|
61
|
+
}
|
|
62
|
+
for (const sub of help.subcommands ?? []) {
|
|
63
|
+
const child = command.command(sub.name).description(sub.description);
|
|
64
|
+
applyOptions(child, sub.options);
|
|
65
|
+
if (sub.helpText) {
|
|
66
|
+
child.addHelpText("after", sub.helpText);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Return a subcommand by name, throwing if absent. */
|
|
72
|
+
export function subcommand(parent: Command, name: string): Command {
|
|
73
|
+
const found = parent.commands.find((c) => c.name() === name);
|
|
74
|
+
if (!found) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Subcommand "${name}" not found on "${parent.name()}" — is it declared in the command's .help module?`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return found;
|
|
80
|
+
}
|
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
type SystemPromptPersonaOverride,
|
|
75
75
|
} from "../prompts/system-prompt.js";
|
|
76
76
|
import type { ContentBlock, Message } from "../providers/types.js";
|
|
77
|
-
import type { Provider } from "../providers/types.js";
|
|
77
|
+
import type { Provider, ToolDefinition } from "../providers/types.js";
|
|
78
78
|
import { type TrustClass } from "../runtime/actor-trust-resolver.js";
|
|
79
79
|
import { broadcastMessage } from "../runtime/assistant-event-hub.js";
|
|
80
80
|
import type { AuthContext } from "../runtime/auth/types.js";
|
|
@@ -768,9 +768,6 @@ export class Conversation {
|
|
|
768
768
|
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
769
769
|
toolExecutor: toolDefs.length > 0 ? toolExecutor : undefined,
|
|
770
770
|
resolveTools,
|
|
771
|
-
// A tool the registry marks exclusive (e.g. `advisor`) runs alone in its
|
|
772
|
-
// turn; the loop defers any sibling calls until the next turn.
|
|
773
|
-
isExclusiveTool: (name) => getTool(name)?.exclusive === true,
|
|
774
771
|
resolveConversationDir: () => {
|
|
775
772
|
const conv = getConversation(this.conversationId);
|
|
776
773
|
if (!conv) {
|
|
@@ -2379,6 +2376,26 @@ export class Conversation {
|
|
|
2379
2376
|
return new Set(this.lastResolvedToolNames ?? this.coreToolNames);
|
|
2380
2377
|
}
|
|
2381
2378
|
|
|
2379
|
+
/**
|
|
2380
|
+
* The {@link getRegisteredToolNames} inventory resolved to full definitions
|
|
2381
|
+
* (name, description, input schema), sorted by name. Resolution reads the
|
|
2382
|
+
* live registry so skill/MCP tools — whose definitions are not in the base
|
|
2383
|
+
* turn snapshot — are included. Consumers (e.g. turn-trace telemetry) read
|
|
2384
|
+
* this instead of reaching into the tool registry themselves.
|
|
2385
|
+
*/
|
|
2386
|
+
getRegisteredToolDefinitions(): ToolDefinition[] {
|
|
2387
|
+
return Array.from(this.getRegisteredToolNames())
|
|
2388
|
+
.sort((a, b) => a.localeCompare(b))
|
|
2389
|
+
.map((name) => {
|
|
2390
|
+
const tool = getTool(name);
|
|
2391
|
+
return {
|
|
2392
|
+
name,
|
|
2393
|
+
description: tool?.description ?? "",
|
|
2394
|
+
input_schema: tool?.input_schema ?? {},
|
|
2395
|
+
};
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2382
2399
|
// ── History ──────────────────────────────────────────────────────
|
|
2383
2400
|
|
|
2384
2401
|
getMessages(): Message[] {
|
package/src/plugin-api/index.ts
CHANGED
|
@@ -180,6 +180,11 @@ export {
|
|
|
180
180
|
getAssistantName,
|
|
181
181
|
resolveUserName,
|
|
182
182
|
} from "../daemon/identity-helpers.js";
|
|
183
|
+
// Declarative help for the top-level `assistant` CLI commands that have adopted
|
|
184
|
+
// the static-help split. Plugins (e.g. the memory capability indexer) read this
|
|
185
|
+
// to embed CLI command capabilities without importing the CLI action graph.
|
|
186
|
+
// Pure data — iterate the fields directly.
|
|
187
|
+
export { CLI_COMMAND_HELP } from "../cli/index.help.js";
|
|
183
188
|
// Embeddings — self-contained operations on the host's shared embedding /
|
|
184
189
|
// vector-store subsystem. Host-resolved: each reads the live workspace config
|
|
185
190
|
// internally, so plugins hold no config. Async because the facade loads the
|
|
@@ -109,6 +109,12 @@ mock.module("../../../../../cli/program.js", () => ({
|
|
|
109
109
|
},
|
|
110
110
|
}));
|
|
111
111
|
|
|
112
|
+
// Keep the suite driven entirely by the mocked program tree above: no commands
|
|
113
|
+
// are sourced from the declarative `cli/index.help.ts` aggregate.
|
|
114
|
+
mock.module("../../../../../cli/index.help.js", () => ({
|
|
115
|
+
CLI_COMMAND_HELP: [],
|
|
116
|
+
}));
|
|
117
|
+
|
|
112
118
|
mock.module(
|
|
113
119
|
"../../../../../persistence/embeddings/embedding-backend.js",
|
|
114
120
|
() => ({
|
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* size without trouble, and trimming would drop the very examples and flag
|
|
11
11
|
* descriptions that make commands semantically findable.
|
|
12
12
|
*/
|
|
13
|
+
|
|
14
|
+
import type { CLI_COMMAND_HELP } from "@vellumai/plugin-api";
|
|
15
|
+
|
|
16
|
+
/** Element type of the plugin-api's declarative CLI help constant. */
|
|
17
|
+
type CliCommandHelp = (typeof CLI_COMMAND_HELP)[number];
|
|
18
|
+
|
|
13
19
|
export function buildCliCommandContent(
|
|
14
20
|
name: string,
|
|
15
21
|
description: string,
|
|
@@ -17,3 +23,49 @@ export function buildCliCommandContent(
|
|
|
17
23
|
): string {
|
|
18
24
|
return `The "assistant ${name}" CLI command is available. ${description}.\n\nFull help:\n${helpText}`;
|
|
19
25
|
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render capability content from a command's declarative {@link CliCommandHelp}
|
|
29
|
+
* (flags, subcommands, help prose) for commands that have adopted the static-help
|
|
30
|
+
* split. Produces the same prose lead-in as {@link buildCliCommandContent} with a
|
|
31
|
+
* data-derived help body, so commands read declaratively are indexed the same way
|
|
32
|
+
* as those still read from the Commander tree.
|
|
33
|
+
*/
|
|
34
|
+
export function buildCliCommandHelpContent(help: CliCommandHelp): string {
|
|
35
|
+
const sections: string[] = [];
|
|
36
|
+
const topOptions = renderOptionLines(help.options);
|
|
37
|
+
if (topOptions) {
|
|
38
|
+
sections.push(topOptions);
|
|
39
|
+
}
|
|
40
|
+
if (help.helpText) {
|
|
41
|
+
sections.push(help.helpText.trim());
|
|
42
|
+
}
|
|
43
|
+
for (const sub of help.subcommands ?? []) {
|
|
44
|
+
const lines = [`${help.name} ${sub.name} — ${sub.description}`];
|
|
45
|
+
const options = renderOptionLines(sub.options);
|
|
46
|
+
if (options) {
|
|
47
|
+
lines.push(options);
|
|
48
|
+
}
|
|
49
|
+
if (sub.helpText) {
|
|
50
|
+
lines.push(sub.helpText.trim());
|
|
51
|
+
}
|
|
52
|
+
sections.push(lines.join("\n"));
|
|
53
|
+
}
|
|
54
|
+
return buildCliCommandContent(
|
|
55
|
+
help.name,
|
|
56
|
+
help.description,
|
|
57
|
+
sections.join("\n\n"),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderOptionLines(options: CliCommandHelp["options"]): string | null {
|
|
62
|
+
if (!options?.length) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return options
|
|
66
|
+
.map(
|
|
67
|
+
(option) =>
|
|
68
|
+
` ${option.flags}${option.required ? " (required)" : ""} ${option.description}`,
|
|
69
|
+
)
|
|
70
|
+
.join("\n");
|
|
71
|
+
}
|
|
@@ -20,12 +20,17 @@
|
|
|
20
20
|
// - No MCP-style augmentation — Commander's description is the canonical
|
|
21
21
|
// summary.
|
|
22
22
|
|
|
23
|
+
import { CLI_COMMAND_HELP } from "@vellumai/plugin-api";
|
|
24
|
+
|
|
23
25
|
import { getConfig } from "../../../../config/loader.js";
|
|
24
26
|
import { generateSparseEmbedding } from "../../../../persistence/embeddings/embedding-backend.js";
|
|
25
27
|
import { applyCorrectionIfCalibrated } from "../anisotropy.js";
|
|
26
28
|
import { embedWithBackend } from "../embeddings.js";
|
|
27
29
|
import { getLogger } from "../logging.js";
|
|
28
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
buildCliCommandContent,
|
|
32
|
+
buildCliCommandHelpContent,
|
|
33
|
+
} from "./cli-command-content.js";
|
|
29
34
|
import { invalidatePageIndex } from "./page-index.js";
|
|
30
35
|
import {
|
|
31
36
|
backfillKindOnPointsWithPrefix,
|
|
@@ -135,21 +140,37 @@ function resolveSeedWaiters(): void {
|
|
|
135
140
|
async function runSeedV2CliCommandEntries(generation: number): Promise<void> {
|
|
136
141
|
try {
|
|
137
142
|
const config = getConfig();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
143
|
+
const seeds: CliCommandEntry[] = [];
|
|
144
|
+
const declarativeNames = new Set<string>();
|
|
145
|
+
|
|
146
|
+
// Commands that have adopted the static-help split are read from their
|
|
147
|
+
// declarative help (exposed via `@vellumai/plugin-api`) — pure data, no CLI
|
|
148
|
+
// action graph. Content is rendered from that data here in the plugin.
|
|
149
|
+
for (const help of CLI_COMMAND_HELP) {
|
|
150
|
+
declarativeNames.add(help.name);
|
|
151
|
+
seeds.push({
|
|
152
|
+
id: help.name,
|
|
153
|
+
description: help.description,
|
|
154
|
+
content: buildCliCommandHelpContent(help),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Remaining commands still come from the Commander tree. Dynamic import so
|
|
159
|
+
// callers that only need `getCliCommandCapability` or `listCliCommandEntries`
|
|
160
|
+
// (e.g. the render path inside `injection.ts` and the `page-index.ts`
|
|
161
|
+
// dependency loader) never drag the full CLI command graph into their import
|
|
162
|
+
// tree. The CLI tree pulls in many provider and workspace modules whose
|
|
163
|
+
// presence has been a recurring source of test-mock cascades and
|
|
164
|
+
// circular-import surprises.
|
|
144
165
|
const { buildCliProgramTree } = await import("../../../../cli/program.js");
|
|
145
166
|
const program = buildCliProgramTree();
|
|
146
167
|
|
|
147
|
-
const seeds: CliCommandEntry[] = [];
|
|
148
168
|
for (const cmd of program.commands) {
|
|
149
169
|
const name = cmd.name();
|
|
150
170
|
// Skip the `help` builtin Commander adds automatically — it carries no
|
|
151
|
-
// capability information of its own and
|
|
152
|
-
|
|
171
|
+
// capability information of its own — and commands already sourced
|
|
172
|
+
// declaratively above.
|
|
173
|
+
if (name === "help" || declarativeNames.has(name)) continue;
|
|
153
174
|
const description = cmd.description();
|
|
154
175
|
const content = buildCliCommandContent(
|
|
155
176
|
name,
|
|
@@ -26,28 +26,17 @@ let mockLiveConversation:
|
|
|
26
26
|
| {
|
|
27
27
|
isProcessing: () => boolean;
|
|
28
28
|
getCurrentSystemPrompt?: () => string;
|
|
29
|
-
|
|
29
|
+
getRegisteredToolDefinitions?: () => Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
input_schema: object;
|
|
33
|
+
}>;
|
|
30
34
|
}
|
|
31
35
|
| undefined;
|
|
32
36
|
mock.module("../daemon/conversation-registry.js", () => ({
|
|
33
37
|
findConversation: () => mockLiveConversation,
|
|
34
38
|
}));
|
|
35
39
|
|
|
36
|
-
// Stub the tool registry so trace assembly can resolve descriptions without
|
|
37
|
-
// loading the real tool graph. Returns `undefined` by default; tests override
|
|
38
|
-
// for specific tool names.
|
|
39
|
-
let mockToolDescriptions: Record<string, string> = {};
|
|
40
|
-
mock.module("../tools/registry.js", () => ({
|
|
41
|
-
getTool: (name: string) =>
|
|
42
|
-
mockToolDescriptions[name]
|
|
43
|
-
? { description: mockToolDescriptions[name], input_schema: {} }
|
|
44
|
-
: undefined,
|
|
45
|
-
resolveTool: (name: string) =>
|
|
46
|
-
mockToolDescriptions[name]
|
|
47
|
-
? { description: mockToolDescriptions[name], input_schema: {} }
|
|
48
|
-
: undefined,
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
40
|
import { createConversation } from "../persistence/conversation-crud.js";
|
|
52
41
|
import { getDb } from "../persistence/db-connection.js";
|
|
53
42
|
import { initializeDb } from "../persistence/db-init.js";
|
|
@@ -72,7 +61,6 @@ function purge(): void {
|
|
|
72
61
|
beforeEach(() => {
|
|
73
62
|
purge();
|
|
74
63
|
mockLiveConversation = undefined;
|
|
75
|
-
mockToolDescriptions = {};
|
|
76
64
|
});
|
|
77
65
|
|
|
78
66
|
interface MessageSeed {
|
|
@@ -238,16 +226,16 @@ describe("assembleTurnTrace", () => {
|
|
|
238
226
|
createdAt: 1100,
|
|
239
227
|
});
|
|
240
228
|
|
|
229
|
+
// The conversation resolves + sorts its registered tools; trace assembly
|
|
230
|
+
// just maps them into the trace, so feed it already-resolved definitions.
|
|
241
231
|
mockLiveConversation = {
|
|
242
232
|
isProcessing: () => false,
|
|
243
233
|
getCurrentSystemPrompt: () => "You are a helpful assistant.",
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
file_read: "Read a file",
|
|
250
|
-
// notify_parent intentionally not in the map — description should be ""
|
|
234
|
+
getRegisteredToolDefinitions: () => [
|
|
235
|
+
{ name: "file_read", description: "Read a file", input_schema: {} },
|
|
236
|
+
{ name: "notify_parent", description: "", input_schema: {} },
|
|
237
|
+
{ name: "web_search", description: "Search the web", input_schema: {} },
|
|
238
|
+
],
|
|
251
239
|
};
|
|
252
240
|
|
|
253
241
|
const trace = assembleTurnTrace(boundary(conv.id, "m-user-1", 1000));
|
|
@@ -3,7 +3,6 @@ import { and, asc, eq, gt, lt, lte, or, sql } from "drizzle-orm";
|
|
|
3
3
|
import { findConversation } from "../daemon/conversation-registry.js";
|
|
4
4
|
import { getDb } from "../persistence/db-connection.js";
|
|
5
5
|
import { messages, toolInvocations } from "../persistence/schema/index.js";
|
|
6
|
-
import { getTool } from "../tools/registry.js";
|
|
7
6
|
import { getLogger } from "../util/logger.js";
|
|
8
7
|
import type {
|
|
9
8
|
TurnTrace,
|
|
@@ -315,16 +314,11 @@ export function assembleTurnTrace(boundary: TurnTraceBoundary): TurnTrace {
|
|
|
315
314
|
const systemPrompt = conversation?.getCurrentSystemPrompt() ?? null;
|
|
316
315
|
|
|
317
316
|
const toolDefinitions: TurnTraceToolDefinition[] = conversation
|
|
318
|
-
?
|
|
319
|
-
.
|
|
320
|
-
.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
name,
|
|
324
|
-
description: tool?.description ?? "",
|
|
325
|
-
input_schema: (tool?.input_schema ?? {}) as Record<string, unknown>,
|
|
326
|
-
};
|
|
327
|
-
})
|
|
317
|
+
? conversation.getRegisteredToolDefinitions().map((tool) => ({
|
|
318
|
+
name: tool.name,
|
|
319
|
+
description: tool.description,
|
|
320
|
+
input_schema: tool.input_schema as Record<string, unknown>,
|
|
321
|
+
}))
|
|
328
322
|
: [];
|
|
329
323
|
|
|
330
324
|
return {
|