@vellumai/assistant 0.12.0-staging.1 → 0.12.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/Dockerfile +5 -0
- package/docs/architecture/turn-actor.md +9 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/app-icons/package.json +18 -0
- package/node_modules/@vellumai/app-icons/src/index.test.ts +85 -0
- package/node_modules/@vellumai/app-icons/src/index.ts +387 -0
- package/node_modules/@vellumai/app-icons/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/node_modules/@vellumai/gateway-client/src/__tests__/plugin-admission-denied-contract.test.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/plugin-admission-denied-contract.ts +15 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +31 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +28 -3
- package/openapi.yaml +628 -0
- package/package.json +3 -1
- package/scripts/smoke-container-workspace-dependencies.ts +19 -0
- package/src/__tests__/app-builder-icon-names.test.ts +29 -0
- package/src/__tests__/assistant-attachment-directive.test.ts +4 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-attachments.test.ts +142 -0
- package/src/__tests__/conversation-delete-activation-progress.test.ts +145 -0
- package/src/__tests__/conversation-event-sink.test.ts +50 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +10 -2
- package/src/__tests__/conversation-queue.test.ts +297 -0
- package/src/__tests__/credential-routes.test.ts +185 -6
- package/src/__tests__/drain-kick-guard.test.ts +2 -0
- package/src/__tests__/drain-requeue-on-contention.test.ts +6 -0
- package/src/__tests__/messaging-send-tool.test.ts +42 -0
- package/src/__tests__/oauth-commands-routes.test.ts +47 -0
- package/src/__tests__/subagent-manager-notify.test.ts +25 -4
- package/src/activation/progress-store.test.ts +1564 -0
- package/src/activation/progress-store.ts +1296 -0
- package/src/activation/turn-hooks.test.ts +246 -0
- package/src/activation/turn-hooks.ts +126 -0
- package/src/api/events/subagent-status-changed.ts +7 -5
- package/src/api/responses/activation.ts +136 -0
- package/src/apps/app-store.ts +8 -0
- package/src/cli/__tests__/catalog-search-help.test.ts +7 -5
- package/src/cli/commands/__tests__/cli-test-harness.ts +12 -3
- package/src/cli/commands/channels/__tests__/channels.test.ts +2 -0
- package/src/cli/commands/channels/__tests__/request.test.ts +191 -0
- package/src/cli/commands/channels/index.help.ts +70 -18
- package/src/cli/commands/channels/index.ts +17 -6
- package/src/cli/commands/channels/request.ts +66 -0
- package/src/cli/commands/oauth/request.test.ts +2 -0
- package/src/cli/commands/oauth/request.ts +227 -186
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -1
- package/src/config/bundled-skills/app-builder/TOOLS.json +2 -2
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +8 -4
- package/src/config/feature-flag-registry.json +35 -5
- package/src/daemon/assistant-attachments.ts +11 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +5 -0
- package/src/daemon/conversation-agent-loop.ts +71 -3
- package/src/daemon/conversation-attachments.ts +42 -1
- package/src/daemon/conversation-event-sink.ts +25 -0
- package/src/daemon/conversation-process.ts +69 -21
- package/src/daemon/conversation-store.ts +4 -3
- package/src/daemon/conversation-surfaces.ts +19 -4
- package/src/daemon/message-types/sync.ts +2 -0
- package/src/ipc/assistant-server.ts +2 -0
- package/src/ipc/routes/__tests__/activation-sync-ipc-routes.test.ts +51 -0
- package/src/ipc/routes/activation-sync-ipc-routes.ts +42 -0
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/proactive-home-thread.test.ts +111 -0
- package/src/notifications/conversation-pairing.ts +47 -5
- package/src/notifications/delivered-post-record.ts +3 -0
- package/src/persistence/__tests__/slack-thread-root-evidence.test.ts +102 -0
- package/src/persistence/conversation-crud.ts +39 -0
- package/src/persistence/delivery-crud.ts +13 -0
- package/src/plugins/AGENTS.md +1 -0
- package/src/runtime/auth/__tests__/route-policy.test.ts +37 -0
- package/src/runtime/routes/__tests__/user-routes-notices.test.ts +248 -0
- package/src/runtime/routes/activation-routes.test.ts +415 -0
- package/src/runtime/routes/activation-routes.ts +172 -0
- package/src/runtime/routes/credential-routes.ts +46 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/oauth-commands-routes.ts +21 -8
- package/src/runtime/routes/platform-managed-credentials.ts +48 -0
- package/src/runtime/routes/secret-routes.ts +3 -12
- package/src/runtime/routes/user-route-resolution.ts +21 -0
- package/src/runtime/routes/user-routes.ts +112 -9
- package/src/runtime/sync/activation-sidecar-publish.test.ts +78 -0
- package/src/runtime/sync/documents-sidecar-publish.test.ts +3 -0
- package/src/runtime/sync/resource-sync-events.ts +23 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +36 -0
- package/src/runtime/sync/worker-daemon-notify.ts +39 -1
- package/src/tools/apps/executors.ts +8 -8
|
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
|
|
3
3
|
import type { Command } from "commander";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { exitCodeFromIpcResult } from "../../../ipc/cli-client.js";
|
|
6
6
|
import {
|
|
7
7
|
findContentTypeHeader,
|
|
8
8
|
parseRequestBodyBytes,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "../../../util/oauth-request-body.js";
|
|
11
11
|
import { readStdinBytesSync } from "../../../util/read-stdin.js";
|
|
12
12
|
import { subcommand } from "../../lib/cli-command-help.js";
|
|
13
|
-
import { shouldOutputJson, writeOutput } from "../../output.js";
|
|
13
|
+
import { shouldOutputJson, writeError, writeOutput } from "../../output.js";
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Helpers
|
|
@@ -72,16 +72,35 @@ export function readBodyData(
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
// ---------------------------------------------------------------------------
|
|
75
|
-
//
|
|
75
|
+
// The authenticated request, shared by every command that makes one
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The request-shaping options every authenticated-request command offers:
|
|
80
|
+
* method, headers, body, output, and verbosity. What differs between the
|
|
81
|
+
* commands is only how the provider is named, so that flag is the caller's.
|
|
82
|
+
*/
|
|
83
|
+
export interface AuthenticatedRequestOptions {
|
|
84
|
+
request?: string;
|
|
85
|
+
header: string[];
|
|
86
|
+
data?: string;
|
|
87
|
+
get?: boolean;
|
|
88
|
+
head?: boolean;
|
|
89
|
+
output?: string;
|
|
90
|
+
silent?: boolean;
|
|
91
|
+
verbose?: boolean;
|
|
92
|
+
include?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Attach the request-shaping options to a subcommand. Registered
|
|
97
|
+
* imperatively (not in a help contract): the repeatable "-H, --header" flag
|
|
98
|
+
* needs a Commander collect parser (function + array default) that the
|
|
99
|
+
* declarative help contract cannot express, and option order around it must
|
|
100
|
+
* be preserved for help output.
|
|
101
|
+
*/
|
|
102
|
+
export function attachRequestOptions(command: Command): Command {
|
|
103
|
+
return command
|
|
85
104
|
.option("-X, --request <method>", "HTTP method (default: GET)")
|
|
86
105
|
.option(
|
|
87
106
|
"-H, --header <header>",
|
|
@@ -98,198 +117,220 @@ export function registerRequestCommand(oauth: Command): void {
|
|
|
98
117
|
.option("-o, --output <file>", "Write response body to file")
|
|
99
118
|
.option("-s, --silent", "Suppress informational stderr output")
|
|
100
119
|
.option("-v, --verbose", "Show request/response details on stderr")
|
|
101
|
-
.option("-i, --include", "Show response headers on stderr")
|
|
102
|
-
|
|
103
|
-
.option("--client-id <id>", "BYO app client ID disambiguation")
|
|
104
|
-
.action(
|
|
105
|
-
async (
|
|
106
|
-
url: string,
|
|
107
|
-
opts: {
|
|
108
|
-
provider: string;
|
|
109
|
-
request?: string;
|
|
110
|
-
header: string[];
|
|
111
|
-
data?: string;
|
|
112
|
-
get?: boolean;
|
|
113
|
-
head?: boolean;
|
|
114
|
-
output?: string;
|
|
115
|
-
silent?: boolean;
|
|
116
|
-
verbose?: boolean;
|
|
117
|
-
include?: boolean;
|
|
118
|
-
account?: string;
|
|
119
|
-
clientId?: string;
|
|
120
|
-
},
|
|
121
|
-
cmd: Command,
|
|
122
|
-
) => {
|
|
123
|
-
const jsonMode = shouldOutputJson(cmd);
|
|
120
|
+
.option("-i, --include", "Show response headers on stderr");
|
|
121
|
+
}
|
|
124
122
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
123
|
+
/**
|
|
124
|
+
* Make one authenticated request through a provider and print the outcome.
|
|
125
|
+
*
|
|
126
|
+
* The provider is named by key; the route resolves its connection and injects
|
|
127
|
+
* its credential, so the caller never handles a token. `account` and
|
|
128
|
+
* `clientId` disambiguate a multi-account OAuth integration and mean nothing
|
|
129
|
+
* for a channel's bot credential, so a caller that cannot have them omits
|
|
130
|
+
* them. `diagnosticsHint` names the command a person runs next when the
|
|
131
|
+
* request fails, in the vocabulary of the command that made it.
|
|
132
|
+
*/
|
|
133
|
+
export async function runAuthenticatedRequest(params: {
|
|
134
|
+
providerKey: string;
|
|
135
|
+
url: string;
|
|
136
|
+
opts: AuthenticatedRequestOptions;
|
|
137
|
+
account?: string;
|
|
138
|
+
clientId?: string;
|
|
139
|
+
diagnosticsHint: string;
|
|
140
|
+
cmd: Command;
|
|
141
|
+
}): Promise<void> {
|
|
142
|
+
const { providerKey, url, opts, cmd } = params;
|
|
143
|
+
const jsonMode = shouldOutputJson(cmd);
|
|
138
144
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
// Helper: write info to stderr (respects -s)
|
|
146
|
+
const writeInfo = (msg: string): void => {
|
|
147
|
+
if (!opts.silent) {
|
|
148
|
+
process.stderr.write(msg + "\n");
|
|
149
|
+
}
|
|
150
|
+
};
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
try {
|
|
153
|
+
// Parse headers for verbose output (before sending to daemon)
|
|
154
|
+
const parsedHeaders: Record<string, string> = {};
|
|
155
|
+
for (const raw of opts.header) {
|
|
156
|
+
const [key, value] = parseHeader(raw);
|
|
157
|
+
parsedHeaders[key] = value;
|
|
158
|
+
}
|
|
153
159
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
// Verbose: show request details
|
|
161
|
+
if (opts.verbose) {
|
|
162
|
+
const method = opts.head
|
|
163
|
+
? "HEAD"
|
|
164
|
+
: opts.request
|
|
165
|
+
? opts.request.toUpperCase()
|
|
166
|
+
: opts.get
|
|
167
|
+
? "GET"
|
|
168
|
+
: opts.data !== undefined
|
|
169
|
+
? "POST"
|
|
170
|
+
: "GET";
|
|
171
|
+
writeInfo(`> ${method} ${url}`);
|
|
172
|
+
for (const [key, value] of Object.entries(parsedHeaders)) {
|
|
173
|
+
writeInfo(`> ${key}: ${value}`);
|
|
174
|
+
}
|
|
175
|
+
writeInfo(`> Authorization: Bearer [REDACTED]`);
|
|
176
|
+
writeInfo(`>`);
|
|
177
|
+
}
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
// Read body data on the CLI side (file/stdin reading must happen here)
|
|
180
|
+
let parsedData: unknown;
|
|
181
|
+
if (opts.data !== undefined) {
|
|
182
|
+
parsedData = readBodyData(opts.data, parsedHeaders);
|
|
183
|
+
}
|
|
178
184
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
185
|
+
const body: Record<string, unknown> = {
|
|
186
|
+
provider: providerKey,
|
|
187
|
+
url,
|
|
188
|
+
};
|
|
189
|
+
if (opts.request) {
|
|
190
|
+
body.method = opts.request;
|
|
191
|
+
}
|
|
192
|
+
if (Object.keys(parsedHeaders).length > 0) {
|
|
193
|
+
body.headers = parsedHeaders;
|
|
194
|
+
}
|
|
195
|
+
if (parsedData !== undefined) {
|
|
196
|
+
body.parsed_data = parsedData;
|
|
197
|
+
}
|
|
198
|
+
if (opts.get) {
|
|
199
|
+
body.force_get = true;
|
|
200
|
+
}
|
|
201
|
+
if (opts.head) {
|
|
202
|
+
body.head = true;
|
|
203
|
+
}
|
|
204
|
+
if (params.account) {
|
|
205
|
+
body.account = params.account;
|
|
206
|
+
}
|
|
207
|
+
if (params.clientId) {
|
|
208
|
+
body.client_id = params.clientId;
|
|
209
|
+
}
|
|
204
210
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await import("../../../runtime/routes/errors.js");
|
|
211
|
+
// Run the route handler in this process so Gmail-sized fetch and
|
|
212
|
+
// JSON parse stay off the assistant event loop.
|
|
213
|
+
const { handleRequest } =
|
|
214
|
+
await import("../../../runtime/routes/oauth-commands-routes.js");
|
|
215
|
+
const { RouteError } = await import("../../../runtime/routes/errors.js");
|
|
211
216
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
217
|
+
let result: {
|
|
218
|
+
ok: boolean;
|
|
219
|
+
status: number;
|
|
220
|
+
headers: Record<string, string>;
|
|
221
|
+
body: unknown;
|
|
222
|
+
bodyEncoding?: "base64";
|
|
223
|
+
hint?: string;
|
|
224
|
+
account?: string | null;
|
|
225
|
+
accountWarning?: string;
|
|
226
|
+
};
|
|
227
|
+
try {
|
|
228
|
+
result = (await handleRequest({ body })) as typeof result;
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (err instanceof RouteError) {
|
|
231
|
+
// A structured route failure (unknown provider, no connection) is
|
|
232
|
+
// reported the way every other failure here is, so `--json` gets its
|
|
233
|
+
// envelope and the caller's diagnostics hint is not lost; only the
|
|
234
|
+
// exit code comes from the route's status.
|
|
235
|
+
writeError(cmd, `${err.message}\n\n${params.diagnosticsHint}`);
|
|
236
|
+
process.exitCode = exitCodeFromIpcResult({
|
|
237
|
+
statusCode: err.statusCode,
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
234
243
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
244
|
+
// Non-2xx exit code
|
|
245
|
+
if (result.status < 200 || result.status >= 300) {
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
}
|
|
239
248
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
249
|
+
// Which account served the request, and any multi-account ambiguity.
|
|
250
|
+
if (result.account) {
|
|
251
|
+
writeInfo(`* Account: ${result.account}`);
|
|
252
|
+
}
|
|
253
|
+
if (result.accountWarning) {
|
|
254
|
+
writeInfo(result.accountWarning);
|
|
255
|
+
}
|
|
247
256
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
// Auth hint
|
|
258
|
+
if (result.hint) {
|
|
259
|
+
writeInfo(result.hint);
|
|
260
|
+
}
|
|
252
261
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
262
|
+
// JSON output mode
|
|
263
|
+
if (jsonMode) {
|
|
264
|
+
writeOutput(cmd, result);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
258
267
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
// Verbose / include: response headers to stderr
|
|
269
|
+
if (opts.verbose || opts.include) {
|
|
270
|
+
writeInfo(`< HTTP ${result.status}`);
|
|
271
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
272
|
+
writeInfo(`< ${key}: ${value}`);
|
|
273
|
+
}
|
|
274
|
+
writeInfo(`<`);
|
|
275
|
+
}
|
|
267
276
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
} else if (opts.output) {
|
|
284
|
-
writeFileSync(opts.output, Buffer.alloc(0));
|
|
277
|
+
// Body output (skip for null bodies: HEAD requests, 204, etc.)
|
|
278
|
+
if (result.body != null || result.bodyEncoding === "base64") {
|
|
279
|
+
const { materializeOAuthRequestOutput } =
|
|
280
|
+
await import("../../../oauth/connection.js");
|
|
281
|
+
const output = materializeOAuthRequestOutput(result);
|
|
282
|
+
if (output) {
|
|
283
|
+
if (opts.output) {
|
|
284
|
+
writeFileSync(opts.output, output.bytes);
|
|
285
|
+
} else {
|
|
286
|
+
process.stdout.write(output.bytes);
|
|
287
|
+
if (!output.isBinary) {
|
|
288
|
+
process.stdout.write("\n");
|
|
285
289
|
}
|
|
286
|
-
} catch (err) {
|
|
287
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
288
|
-
writeError(
|
|
289
|
-
`Error: ${message}\n\n` +
|
|
290
|
-
`For provider diagnostics, run 'assistant oauth providers get ${opts.provider}'.`,
|
|
291
|
-
);
|
|
292
290
|
}
|
|
291
|
+
}
|
|
292
|
+
} else if (opts.output) {
|
|
293
|
+
writeFileSync(opts.output, Buffer.alloc(0));
|
|
294
|
+
}
|
|
295
|
+
} catch (err) {
|
|
296
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
297
|
+
writeError(cmd, `${message}\n\n${params.diagnosticsHint}`);
|
|
298
|
+
process.exitCode = 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
// Command registration
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
export function registerRequestCommand(oauth: Command): void {
|
|
307
|
+
attachRequestOptions(
|
|
308
|
+
subcommand(oauth, "request").requiredOption(
|
|
309
|
+
"--provider <key>",
|
|
310
|
+
"Provider name (e.g. google, slack)",
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
.option("--account <account>", "Account identifier for multi-account")
|
|
314
|
+
.option("--client-id <id>", "BYO app client ID disambiguation")
|
|
315
|
+
.action(
|
|
316
|
+
async (
|
|
317
|
+
url: string,
|
|
318
|
+
opts: AuthenticatedRequestOptions & {
|
|
319
|
+
provider: string;
|
|
320
|
+
account?: string;
|
|
321
|
+
clientId?: string;
|
|
322
|
+
},
|
|
323
|
+
cmd: Command,
|
|
324
|
+
) => {
|
|
325
|
+
await runAuthenticatedRequest({
|
|
326
|
+
providerKey: opts.provider,
|
|
327
|
+
url,
|
|
328
|
+
opts,
|
|
329
|
+
account: opts.account,
|
|
330
|
+
clientId: opts.clientId,
|
|
331
|
+
diagnosticsHint: `For provider diagnostics, run 'assistant oauth providers get ${opts.provider}'.`,
|
|
332
|
+
cmd,
|
|
333
|
+
});
|
|
293
334
|
},
|
|
294
335
|
);
|
|
295
336
|
}
|
|
@@ -197,7 +197,7 @@ Anything else fails with `Invalid input for tool "app_create": Unknown parameter
|
|
|
197
197
|
|
|
198
198
|
- **`html`** — old single-file shortcut. Put your HTML inside `source_files["src/index.html"]`.
|
|
199
199
|
- **`pages`** — retired. Multi-page apps use TSX components under `src/components/`.
|
|
200
|
-
- **`icon
|
|
200
|
+
- **`icon`**: NOT a top-level param. The icon goes in `preview.icon` as a Lucide icon name from the list below (e.g. `preview: { title: "Bean Coffee", icon: "coffee" }`). Pick the one that best says what the app is; it is drawn in the sidebar and the library, so an emoji or a URL is wrong here. For an AI-generated image icon, call `app_generate_icon(app_id, description)` *after* the app exists.
|
|
201
201
|
- **A file path as a top-level key** (e.g. `"src/components/Header.tsx"`) — these go inside `source_files`, or in a `file_write` after `app_create`.
|
|
202
202
|
|
|
203
203
|
If a prior session in your context shows `app_create({ html })` or `app_create({ pages })`, that example is outdated — ignore it.
|
|
@@ -217,6 +217,8 @@ app_create({ app_create({
|
|
|
217
217
|
|
|
218
218
|
**Key notes:** `preview` — always include, `title` required (plus optional `subtitle`, `description`, `icon`, up to 3 `metrics`). `auto_open` — **always pass `false`** so you don't get a duplicate preview card (Step 5 owns surfacing).
|
|
219
219
|
|
|
220
|
+
**App icon names** (`preview.icon`, kebab-case, one of): `calculator`, `calendar`, `list-todo`, `list-checks`, `square-check`, `timer`, `clock`, `alarm-clock`, `notebook-pen`, `sticky-note`, `pencil`, `file-text`, `clipboard-list`, `bookmark`, `book`, `book-open`, `chart-bar`, `chart-line`, `chart-pie`, `table`, `square-kanban`, `database`, `gauge`, `activity`, `target`, `flag`, `trophy`, `wallet`, `dollar-sign`, `piggy-bank`, `credit-card`, `receipt`, `percent`, `shopping-cart`, `package`, `gift`, `ticket`, `mail`, `inbox`, `message-square`, `phone`, `bell`, `users`, `contact`, `music`, `headphones`, `mic`, `video`, `film`, `tv`, `play`, `image`, `camera`, `gamepad-2`, `puzzle`, `party-popper`, `smile`, `map`, `map-pin`, `compass`, `globe`, `plane`, `car`, `bus`, `bike`, `ship`, `truck`, `house`, `bed`, `briefcase`, `graduation-cap`, `languages`, `brain`, `lightbulb`, `heart`, `heart-pulse`, `dumbbell`, `pill`, `stethoscope`, `baby`, `paw-print`, `utensils`, `coffee`, `wine`, `beer`, `cake`, `apple`, `carrot`, `salad`, `egg`, `fish`, `cloud`, `sun`, `moon`, `umbrella`, `snowflake`, `thermometer`, `droplets`, `flame`, `leaf`, `mountain`, `code`, `terminal`, `cpu`, `bot`, `wifi`, `lock`, `key`, `shield`, `settings`, `wrench`, `plug`, `battery`, `search`, `link`, `hash`, `layers`, `folder-open`, `palette`, `pen-tool`, `ruler`, `scale`, `scissors`, `shirt`, `newspaper`, `repeat`, `shuffle`, `volume-2`, `speaker`, `star`, `sparkles`, `zap`, `rocket`, `home`.
|
|
221
|
+
|
|
220
222
|
### 4 — Compile
|
|
221
223
|
|
|
222
224
|
```
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"icon": {
|
|
45
45
|
"type": "string",
|
|
46
|
-
"description": "Optional icon
|
|
46
|
+
"description": "Optional icon for the app, as a Lucide icon name from the app icon list in the app-builder skill (kebab-case, e.g. calculator, calendar, list-todo, timer, notebook-pen, chart-bar, wallet, mail, map-pin, dumbbell). Pick the one that best says what the app is; it is drawn in the sidebar and the library. Not an emoji and not a URL: for an image icon call app_generate_icon after creating the app."
|
|
47
47
|
},
|
|
48
48
|
"metrics": {
|
|
49
49
|
"type": "array",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
},
|
|
74
74
|
"icon": {
|
|
75
75
|
"type": "string",
|
|
76
|
-
"description": "Lenient alias. Prefer preview.icon.
|
|
76
|
+
"description": "Lenient alias. Prefer preview.icon. A Lucide icon name passed here is folded into preview.icon automatically."
|
|
77
77
|
},
|
|
78
78
|
"html": {
|
|
79
79
|
"type": "string",
|
|
@@ -76,10 +76,10 @@ const ATTACHMENT_CAPABLE_PLATFORMS = new Set(["gmail", "outlook"]);
|
|
|
76
76
|
* home: the turn arrived on a channel, in a chat, in a thread (the turn-local
|
|
77
77
|
* snapshot on the tool context), and a post the provider delivered to exactly
|
|
78
78
|
* that place is a same-conversation send even when the chat's home resolves
|
|
79
|
-
* elsewhere
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
79
|
+
* elsewhere. The delivered thread is the one the provider reports, not the
|
|
80
|
+
* one requested: a provider that ignores the request lands the post in the
|
|
81
|
+
* thread-less chat, and the record must say so; a post into a thread is
|
|
82
|
+
* recorded in the thread's own conversation, where its replies arrive. The home
|
|
83
83
|
* comparison stays as the second test, for a sender that arrived through no
|
|
84
84
|
* channel but is the home. The post is then in the outbound index only
|
|
85
85
|
* through no path, which is the same class as a raw API send and is deferred
|
|
@@ -114,6 +114,7 @@ async function recordSentChannelPost(params: {
|
|
|
114
114
|
const home = await resolveProactiveHomeConversation({
|
|
115
115
|
sourceChannel: providerId,
|
|
116
116
|
externalChatId,
|
|
117
|
+
threadId: params.deliveredThreadId,
|
|
117
118
|
source: "notification",
|
|
118
119
|
conversationType: "background",
|
|
119
120
|
title: `Messages to ${externalChatId}`,
|
|
@@ -125,6 +126,9 @@ async function recordSentChannelPost(params: {
|
|
|
125
126
|
conversationId: home.conversationId,
|
|
126
127
|
channel: providerId,
|
|
127
128
|
externalChatId,
|
|
129
|
+
...(params.deliveredThreadId
|
|
130
|
+
? { threadId: params.deliveredThreadId }
|
|
131
|
+
: {}),
|
|
128
132
|
text: params.text,
|
|
129
133
|
providerMessageId: params.providerMessageId,
|
|
130
134
|
crossPostedFrom: sender.conversationId,
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"label": "In-Chat Onboarding Tour",
|
|
17
17
|
"description": "Multivariate experiment for the eyes-led in-chat onboarding tour. control = straight to chat after research onboarding (current behavior); tour = the extended tour auto-plays on first workspace entry. Desktop-only: phone-width viewports and the native shell get neither the tour nor its telemetry (not even the exposure event), so the funnel measures desktop sessions exclusively. Targeted via LaunchDarkly (70% tour / 30% control).",
|
|
18
18
|
"defaultEnabled": "control",
|
|
19
|
-
"values": [
|
|
19
|
+
"values": [
|
|
20
|
+
"control",
|
|
21
|
+
"tour"
|
|
22
|
+
]
|
|
20
23
|
},
|
|
21
24
|
{
|
|
22
25
|
"id": "user-hosted-enabled",
|
|
@@ -33,7 +36,10 @@
|
|
|
33
36
|
"label": "Proactive Tips",
|
|
34
37
|
"description": "Gates the dismissible proactive tip card in the web sidebar. String-valued so future A/B arms can be added as new values.",
|
|
35
38
|
"defaultEnabled": "off",
|
|
36
|
-
"values": [
|
|
39
|
+
"values": [
|
|
40
|
+
"off",
|
|
41
|
+
"on"
|
|
42
|
+
]
|
|
37
43
|
},
|
|
38
44
|
{
|
|
39
45
|
"id": "vision-mode",
|
|
@@ -42,7 +48,10 @@
|
|
|
42
48
|
"label": "Vision Mode",
|
|
43
49
|
"description": "Gates hold-to-Live ambient frame sampling in the voice room camera. String-valued so future A/B arms can be added as new values.",
|
|
44
50
|
"defaultEnabled": "off",
|
|
45
|
-
"values": [
|
|
51
|
+
"values": [
|
|
52
|
+
"off",
|
|
53
|
+
"on"
|
|
54
|
+
]
|
|
46
55
|
},
|
|
47
56
|
{
|
|
48
57
|
"id": "assistant-initiated-threads",
|
|
@@ -51,7 +60,10 @@
|
|
|
51
60
|
"label": "Assistant-Initiated Threads",
|
|
52
61
|
"description": "Gates the sidebar section collecting the conversations the assistant started on its own: rows stamped `source='assistant_initiated'` at creation by producers that opt a thread in (heartbeat realizations worth the user's time; nothing stamps it yet, so the section starts empty and holds only new threads by construction). Transactional notification trails keep `source='notification'` and stay in Chats and the bell. When on, the daemon emits an `assistant` row from the section index and withholds member conversations from the Chats bucket; when off, they stay in Chats and no `assistant` row is emitted, which is the shipped behavior. Deliberately daemon-scope only: the index row is self-describing, so clients render the section on its presence rather than on a flag of their own. A second client-side gate could only hide a section whose rows the daemon has already withheld from Chats, which would lose those threads instead of reverting the feature. String-valued so future A/B arms can be added as new values.",
|
|
53
62
|
"defaultEnabled": "off",
|
|
54
|
-
"values": [
|
|
63
|
+
"values": [
|
|
64
|
+
"off",
|
|
65
|
+
"on"
|
|
66
|
+
]
|
|
55
67
|
},
|
|
56
68
|
{
|
|
57
69
|
"id": "experiment-activation-flow-2026-06-03",
|
|
@@ -60,7 +72,11 @@
|
|
|
60
72
|
"label": "Activation Flow Experiment 2026-06-03",
|
|
61
73
|
"description": "Multivariate activation-flow experiment. control = standard flow; variant-a = activation rail; personal-page = new sign-up-page variant (front-end only). Targeted via LaunchDarkly.",
|
|
62
74
|
"defaultEnabled": "control",
|
|
63
|
-
"values": [
|
|
75
|
+
"values": [
|
|
76
|
+
"control",
|
|
77
|
+
"variant-a",
|
|
78
|
+
"personal-page"
|
|
79
|
+
]
|
|
64
80
|
},
|
|
65
81
|
{
|
|
66
82
|
"id": "local-docker-enabled",
|
|
@@ -486,6 +502,20 @@
|
|
|
486
502
|
"description": "Platform pods resolve webhook callback URLs from the Velay-published ingress URL instead of registering platform callback routes. Falls back to platform callback registration while no tunnel URL is published.",
|
|
487
503
|
"defaultEnabled": false
|
|
488
504
|
},
|
|
505
|
+
{
|
|
506
|
+
"id": "experiment-activation-checklist-2026-09-10",
|
|
507
|
+
"scope": "client",
|
|
508
|
+
"key": "experiment-activation-checklist-2026-09-10",
|
|
509
|
+
"label": "Experiment: Activation Checklist (2026-09-10)",
|
|
510
|
+
"description": "A/B test of the post-onboarding activation checklist (welcome modal, suggestions pill, inspiration list). off = control, the standard chat landing with no checklist; smb / parent / general = the checklist with that persona's task list. Any other value hides the checklist. Percentages per arm are set in LaunchDarkly; the readout keys a user on their first-ever assignment, so a new round gets a new key.",
|
|
511
|
+
"defaultEnabled": "off",
|
|
512
|
+
"values": [
|
|
513
|
+
"off",
|
|
514
|
+
"smb",
|
|
515
|
+
"parent",
|
|
516
|
+
"general"
|
|
517
|
+
]
|
|
518
|
+
},
|
|
489
519
|
{
|
|
490
520
|
"id": "send-user-message",
|
|
491
521
|
"scope": "both",
|
|
@@ -33,6 +33,15 @@ export interface AssistantAttachmentDraft {
|
|
|
33
33
|
dataBase64: string;
|
|
34
34
|
sizeBytes: number;
|
|
35
35
|
kind: "image" | "video" | "document";
|
|
36
|
+
/**
|
|
37
|
+
* Absolute path the file was read from, after the directive's path was
|
|
38
|
+
* resolved against its boundary. Absent for drafts derived from tool
|
|
39
|
+
* content blocks, which have no file of their own. Callers that need to
|
|
40
|
+
* point a user back at the produced file (the activation checklist's
|
|
41
|
+
* artifact cards) read it, paired with `sourceType` so a host path is
|
|
42
|
+
* never mistaken for a workspace one.
|
|
43
|
+
*/
|
|
44
|
+
sourcePath?: string;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
// ---------------------------------------------------------------------------
|
|
@@ -600,6 +609,7 @@ export function resolveSandboxDirective(
|
|
|
600
609
|
dataBase64,
|
|
601
610
|
sizeBytes: data.length,
|
|
602
611
|
kind: classifyKind(mimeType),
|
|
612
|
+
sourcePath: resolved,
|
|
603
613
|
},
|
|
604
614
|
warning: null,
|
|
605
615
|
};
|
|
@@ -716,6 +726,7 @@ export async function resolveHostDirective(
|
|
|
716
726
|
dataBase64,
|
|
717
727
|
sizeBytes: data.length,
|
|
718
728
|
kind: classifyKind(mimeType),
|
|
729
|
+
sourcePath: resolved,
|
|
719
730
|
},
|
|
720
731
|
warning: null,
|
|
721
732
|
};
|