gaoding-cli 1.0.0-alpha.13 → 1.0.0-alpha.15

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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/contracts/operations/agent.send/input.schema.json +4 -0
  3. package/contracts/operations/model.get/output.schema.json +1 -0
  4. package/dist/bin/gd-cli.js +37 -16
  5. package/dist/src/bootstrap/create-cli.js +43 -4
  6. package/dist/src/bootstrap/create-runtime.js +44 -13
  7. package/dist/src/cli/action-binding.js +43 -9
  8. package/dist/src/cli/agent-commands.js +13 -3
  9. package/dist/src/cli/auth-commands.js +21 -13
  10. package/dist/src/cli/dam-commands.js +35 -20
  11. package/dist/src/cli/errors.js +34 -1
  12. package/dist/src/cli/model-commands.js +25 -11
  13. package/dist/src/cli/org-commands.js +18 -12
  14. package/dist/src/cli/presenter.js +11 -1
  15. package/dist/src/cli/tool-commands.js +19 -7
  16. package/dist/src/cli/update-command.js +6 -4
  17. package/dist/src/features/agent/creative-agent-adapter.js +19 -14
  18. package/dist/src/features/agent/creative-protocol.js +7 -3
  19. package/dist/src/features/agent/use-cases.js +63 -54
  20. package/dist/src/features/auth/use-cases.js +82 -65
  21. package/dist/src/features/dam/asset-projection.js +22 -14
  22. package/dist/src/features/dam/dam-api-adapter.js +19 -4
  23. package/dist/src/features/dam/object-storage.js +2 -0
  24. package/dist/src/features/dam/registered-uploader.js +58 -35
  25. package/dist/src/features/dam/use-cases.js +39 -32
  26. package/dist/src/features/org/use-cases.js +27 -11
  27. package/dist/src/features/tool/catalog.js +7 -3
  28. package/dist/src/features/tool/dynamic-schema.js +19 -9
  29. package/dist/src/features/tool/mns-catalog-adapter.js +7 -4
  30. package/dist/src/features/tool/tool-api-adapter.js +186 -73
  31. package/dist/src/features/tool/use-cases.js +63 -34
  32. package/dist/src/features/update/update-service.js +28 -22
  33. package/dist/src/platform/json-input.js +3 -0
  34. package/dist/src/platform/remote-error-evidence.js +66 -0
  35. package/dist/src/platform/remote-protocol.js +10 -0
  36. package/dist/src/platform/signed-http-transport.js +31 -5
  37. package/dist/src/telemetry/invocation.js +156 -0
  38. package/dist/src/telemetry/sls-sink.js +31 -0
  39. package/package.json +1 -1
  40. package/skills/gd-cli/references/creation.md +15 -4
  41. package/skills/gd-cli/references/errors.md +3 -1
@@ -7,9 +7,13 @@ export class DeviceAuthorizationExpiredError extends Error {
7
7
  export function createAuthUseCases(dependencies) {
8
8
  return {
9
9
  async status(input) {
10
- const state = await dependencies.store.read();
10
+ const state = await dependencies.telemetry.stage("state", () => dependencies.store.read());
11
11
  if (!state)
12
12
  return { logged_in: false, next_steps: ["gd-cli auth login"] };
13
+ dependencies.telemetry.annotate({
14
+ account_id: state.account?.id,
15
+ organization_id: state.organization?.id
16
+ });
13
17
  const valid = Date.parse(state.credential.expiresAt) > input.now.getTime();
14
18
  return {
15
19
  logged_in: true,
@@ -33,86 +37,99 @@ export function createAuthUseCases(dependencies) {
33
37
  };
34
38
  },
35
39
  async logout() {
36
- await dependencies.store.remove();
40
+ await dependencies.telemetry.stage("state", () => dependencies.store.remove());
37
41
  },
38
42
  async login(input) {
39
43
  input.signal.throwIfAborted();
40
- const existing = await dependencies.store.read();
44
+ const existing = await dependencies.telemetry.stage("state", () => dependencies.store.read());
41
45
  if (existing && Date.parse(existing.credential.expiresAt) > dependencies.now().getTime()) {
46
+ dependencies.telemetry.annotate({
47
+ account_id: existing.account?.id,
48
+ organization_id: existing.organization?.id
49
+ });
42
50
  return {
43
51
  status: "already_logged_in",
44
52
  organizationSelected: existing.organization !== undefined
45
53
  };
46
54
  }
47
- const authorization = await dependencies.sso.startDeviceAuthorization(input.signal);
48
- const deadline = dependencies.now().getTime() + authorization.expiresInSeconds * 1000;
49
- const authorizationUrl = new URL("/cli/device-authorization", dependencies.authPageOrigin);
50
- authorizationUrl.searchParams.set("code", authorization.authorizeCode);
51
- await input.interaction.showAuthorization(authorizationUrl);
52
- if (!input.noBrowser) {
53
- try {
54
- await input.interaction.openBrowser(authorizationUrl);
55
- }
56
- catch {
57
- input.interaction.warn("无法自动打开浏览器,请手动访问授权链接。");
55
+ const authorization = await dependencies.telemetry.stage("authorize", async () => {
56
+ const authorization = await dependencies.sso.startDeviceAuthorization(input.signal);
57
+ const authorizationUrl = new URL("/cli/device-authorization", dependencies.authPageOrigin);
58
+ authorizationUrl.searchParams.set("code", authorization.authorizeCode);
59
+ await input.interaction.showAuthorization(authorizationUrl);
60
+ if (!input.noBrowser) {
61
+ try {
62
+ await input.interaction.openBrowser(authorizationUrl);
63
+ }
64
+ catch {
65
+ input.interaction.warn("无法自动打开浏览器,请手动访问授权链接。");
66
+ }
58
67
  }
59
- }
68
+ return authorization;
69
+ });
70
+ const deadline = dependencies.now().getTime() + authorization.expiresInSeconds * 1000;
60
71
  let interval = authorization.intervalSeconds * 1000;
61
- while (true) {
62
- if (dependencies.now().getTime() + interval > deadline) {
63
- throw new DeviceAuthorizationExpiredError();
64
- }
65
- await dependencies.sleep(interval, input.signal);
66
- input.signal.throwIfAborted();
67
- const token = await dependencies.sso.pollDeviceToken(authorization.deviceCode, input.signal);
68
- if (token.status === "pending")
69
- continue;
70
- if (token.status === "slow_down") {
71
- interval += 5_000;
72
- continue;
73
- }
74
- let state = {
75
- version: 1,
76
- credential: token.credential,
77
- ...(token.account ? { account: token.account } : {})
78
- };
79
- await dependencies.store.write(state);
80
- let organizationId = token.organizationId;
81
- try {
82
- const identity = await dependencies.sso.getIdentity(token.credential.accessKey, input.signal);
83
- organizationId = identity.organizationId ?? organizationId;
84
- if (!state.account && identity.account) {
85
- state = { ...state, account: identity.account };
86
- await dependencies.store.write(state);
72
+ const token = await dependencies.telemetry.stage("wait", async () => {
73
+ while (true) {
74
+ if (dependencies.now().getTime() + interval > deadline) {
75
+ throw new DeviceAuthorizationExpiredError();
87
76
  }
88
- }
89
- catch {
90
- if (input.signal.aborted)
91
- throw input.signal.reason;
92
- }
93
- if (!organizationId) {
94
- input.interaction.warn("登录成功,请执行 gd-cli org switch 选择组织。");
95
- return { status: "logged_in", organizationSelected: false };
96
- }
97
- try {
98
- const organization = await dependencies.organizationBinder.bindAuthorizedOrganization({
99
- credential: token.credential,
100
- organizationId,
101
- signal: input.signal
102
- });
103
- if (!organization) {
104
- input.interaction.warn("登录成功,但未能选择组织;请执行 gd-cli org switch。");
105
- return { status: "logged_in", organizationSelected: false };
77
+ await dependencies.sleep(interval, input.signal);
78
+ input.signal.throwIfAborted();
79
+ const result = await dependencies.sso.pollDeviceToken(authorization.deviceCode, input.signal);
80
+ if (result.status === "pending")
81
+ continue;
82
+ if (result.status === "slow_down") {
83
+ interval += 5_000;
84
+ continue;
106
85
  }
107
- await dependencies.store.write({ ...state, organization });
108
- return { status: "logged_in", organizationSelected: true };
86
+ return result;
87
+ }
88
+ });
89
+ let state = {
90
+ version: 1,
91
+ credential: token.credential,
92
+ ...(token.account ? { account: token.account } : {})
93
+ };
94
+ await dependencies.telemetry.stage("state", () => dependencies.store.write(state));
95
+ dependencies.telemetry.annotate({ account_id: state.account?.id });
96
+ let organizationId = token.organizationId;
97
+ try {
98
+ const identity = await dependencies.telemetry.stage("identity", () => (dependencies.sso.getIdentity(token.credential.accessKey, input.signal)));
99
+ organizationId = identity.organizationId ?? organizationId;
100
+ if (!state.account && identity.account) {
101
+ state = { ...state, account: identity.account };
102
+ await dependencies.telemetry.stage("state", () => dependencies.store.write(state));
103
+ dependencies.telemetry.annotate({ account_id: identity.account.id });
109
104
  }
110
- catch {
111
- if (input.signal.aborted)
112
- throw input.signal.reason;
113
- input.interaction.warn("登录成功,但组织绑定失败;请执行 gd-cli org switch。");
105
+ }
106
+ catch {
107
+ if (input.signal.aborted)
108
+ throw input.signal.reason;
109
+ }
110
+ if (!organizationId) {
111
+ input.interaction.warn("登录成功,请执行 gd-cli org switch 选择组织。");
112
+ return { status: "logged_in", organizationSelected: false };
113
+ }
114
+ try {
115
+ const organization = await dependencies.telemetry.stage("bind", () => (dependencies.organizationBinder.bindAuthorizedOrganization({
116
+ credential: token.credential,
117
+ organizationId,
118
+ signal: input.signal
119
+ })));
120
+ if (!organization) {
121
+ input.interaction.warn("登录成功,但未能选择组织;请执行 gd-cli org switch。");
114
122
  return { status: "logged_in", organizationSelected: false };
115
123
  }
124
+ await dependencies.telemetry.stage("state", () => dependencies.store.write({ ...state, organization }));
125
+ dependencies.telemetry.annotate({ organization_id: organization.id });
126
+ return { status: "logged_in", organizationSelected: true };
127
+ }
128
+ catch {
129
+ if (input.signal.aborted)
130
+ throw input.signal.reason;
131
+ input.interaction.warn("登录成功,但组织绑定失败;请执行 gd-cli org switch。");
132
+ return { status: "logged_in", organizationSelected: false };
116
133
  }
117
134
  }
118
135
  };
@@ -8,6 +8,16 @@ const publicAssetTypes = new Set([
8
8
  "template",
9
9
  "file"
10
10
  ]);
11
+ const internalAssetTypes = new Map([
12
+ ["gdpic", "image"],
13
+ ["gdimage", "image"],
14
+ ["gdvideo", "video"],
15
+ ["gdaudio", "audio"],
16
+ ["gdfont", "font"],
17
+ ["gdtemplate", "template"],
18
+ ["gdfile", "file"],
19
+ ["gdslide", "file"]
20
+ ]);
11
21
  const recognizableAssetKeys = new Set([
12
22
  "asset_id",
13
23
  "id",
@@ -39,6 +49,12 @@ function record(value) {
39
49
  function nonblank(value) {
40
50
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
41
51
  }
52
+ function publicSourceFormat(value) {
53
+ const format = nonblank(value);
54
+ return format !== undefined && !internalAssetTypes.has(format.toLowerCase())
55
+ ? format
56
+ : undefined;
57
+ }
42
58
  function finiteNumber(value) {
43
59
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
44
60
  }
@@ -114,19 +130,11 @@ function assetType(item, format, mediaType) {
114
130
  const explicit = nonblank(item.type)?.toLowerCase();
115
131
  if (explicit !== undefined && publicAssetTypes.has(explicit))
116
132
  return explicit;
117
- const contentFormat = (nonblank(item.content_format) ?? nonblank(item.storage_format))?.toLowerCase();
118
- if (contentFormat === "gdpic" || contentFormat === "gdimage")
119
- return "image";
120
- if (contentFormat === "gdvideo")
121
- return "video";
122
- if (contentFormat === "gdaudio")
123
- return "audio";
124
- if (contentFormat === "gdfont")
125
- return "font";
126
- if (contentFormat === "gdtemplate")
127
- return "template";
128
- if (contentFormat === "gdfile" || contentFormat === "gdslide")
129
- return "file";
133
+ for (const candidate of [item.content_format, item.storage_format, item.format]) {
134
+ const internalType = internalAssetTypes.get(nonblank(candidate)?.toLowerCase() ?? "");
135
+ if (internalType !== undefined)
136
+ return internalType;
137
+ }
130
138
  const mediaCategory = mediaType?.split("/", 1)[0]?.toLowerCase();
131
139
  if (mediaCategory !== undefined && publicAssetTypes.has(mediaCategory))
132
140
  return mediaCategory;
@@ -160,7 +168,7 @@ function normalizeAsset(value, fallback) {
160
168
  ?? record(item.cover);
161
169
  const repository = record(item.repository_info) ?? record(item.repositoryInfo);
162
170
  const folder = record(item.folder_info) ?? record(item.folderInfo);
163
- const format = nonblank(origin?.format) ?? nonblank(item.format);
171
+ const format = publicSourceFormat(origin?.format) ?? publicSourceFormat(item.format);
164
172
  const mediaType = nonblank(origin?.mime_type)
165
173
  ?? nonblank(item.mime_type)
166
174
  ?? nonblank(item.mimeType);
@@ -1,5 +1,11 @@
1
1
  import { RemoteRequestError } from "../../platform/signed-http-transport.js";
2
2
  import { projectDamAssetDetail, projectDamAssetSummary } from "./asset-projection.js";
3
+ export class DamAssetNotFoundError extends Error {
4
+ constructor() {
5
+ super("DAM 素材不存在。");
6
+ this.name = "DamAssetNotFoundError";
7
+ }
8
+ }
3
9
  const contentFormats = {
4
10
  image: ["gdpic", "gdimage"],
5
11
  video: ["gdvideo"],
@@ -152,10 +158,19 @@ export function createDamApiAdapter(dependencies) {
152
158
  return searchResult(response, input);
153
159
  },
154
160
  async getAsset(input) {
155
- const response = await dependencies.transport.getJson({
156
- path: `/dam/asset/${encodeURIComponent(input.assetId)}`,
157
- ...requestContext(input)
158
- });
161
+ let response;
162
+ try {
163
+ response = await dependencies.transport.getJson({
164
+ path: `/dam/asset/${encodeURIComponent(input.assetId)}`,
165
+ ...requestContext(input)
166
+ });
167
+ }
168
+ catch (error) {
169
+ if (error instanceof RemoteRequestError && error.status === 404) {
170
+ throw new DamAssetNotFoundError();
171
+ }
172
+ throw error;
173
+ }
159
174
  const asset = projectDamAssetDetail(response, input.assetId);
160
175
  if (asset === undefined)
161
176
  throw new RemoteRequestError();
@@ -66,6 +66,8 @@ const defaultPutOss = async (input) => {
66
66
  accessKeyId: input.token.accessKeyId,
67
67
  accessKeySecret: input.token.accessKeySecret,
68
68
  stsToken: input.token.securityToken,
69
+ // DAM issues one-shot credentials without a refresh endpoint.
70
+ refreshSTSTokenInterval: Number.MAX_SAFE_INTEGER,
69
71
  endpoint: input.endpoint,
70
72
  region: input.token.region,
71
73
  bucket: input.token.bucketName,
@@ -114,24 +114,45 @@ export function createDamRegisteredUploader(dependencies) {
114
114
  return {
115
115
  async upload(input) {
116
116
  try {
117
+ const stage = (name, run) => (dependencies.telemetry.stage(name, async () => {
118
+ try {
119
+ return await run();
120
+ }
121
+ catch (error) {
122
+ if (input.signal.aborted)
123
+ throw input.signal.reason;
124
+ if (error instanceof RemoteRequestError)
125
+ throw error;
126
+ throw new RemoteRequestError();
127
+ }
128
+ }));
117
129
  input.signal.throwIfAborted();
118
- const [digest, metadata] = await Promise.all([
119
- hashes(input.file.filePath, input.signal),
120
- localMetadata(input.file, input.signal)
121
- ]);
122
- input.signal.throwIfAborted();
123
- const contentId = await dependencies.api.createAssetId({
124
- access: input.access,
125
- signal: input.signal
130
+ const prepared = await stage("prepare", async () => {
131
+ const [digest, metadata] = await Promise.all([
132
+ hashes(input.file.filePath, input.signal),
133
+ localMetadata(input.file, input.signal)
134
+ ]);
135
+ input.signal.throwIfAborted();
136
+ const contentId = await dependencies.api.createAssetId({
137
+ access: input.access,
138
+ signal: input.signal
139
+ });
140
+ const format = input.file.format.toLowerCase();
141
+ return {
142
+ digest,
143
+ metadata,
144
+ contentId,
145
+ format,
146
+ damFormat: storageFormat(format),
147
+ title: input.input.title ?? filenameTitle(input.file.filename),
148
+ folderId: input.input.folderId ?? "0"
149
+ };
126
150
  });
127
- const stored = await dependencies.storage.upload({
151
+ const { digest, metadata, contentId, format, damFormat, title, folderId } = prepared;
152
+ const stored = await stage("upload", () => dependencies.storage.upload({
128
153
  file: input.file,
129
154
  contentId
130
- }, input.access, input.signal);
131
- const format = input.file.format.toLowerCase();
132
- const damFormat = storageFormat(format);
133
- const title = input.input.title ?? filenameTitle(input.file.filename);
134
- const folderId = input.input.folderId ?? "0";
155
+ }, input.access, input.signal));
135
156
  const preview = input.file.mediaType.startsWith("image/")
136
157
  ? {
137
158
  url: stored.url,
@@ -182,35 +203,37 @@ export function createDamRegisteredUploader(dependencies) {
182
203
  });
183
204
  if (fallback === undefined)
184
205
  throw new RemoteRequestError();
185
- let latest = await dependencies.api.createMaterial({
206
+ let latest = await stage("persist", () => dependencies.api.createMaterial({
186
207
  body,
187
208
  fallback,
188
209
  access: input.access,
189
210
  signal: input.signal
190
- });
211
+ }));
191
212
  if (input.input.waitAnalysis !== true || terminalAnalysisStatus(latest.analysis_status)) {
192
213
  return latest;
193
214
  }
194
- const deadline = now().getTime() + 180_000;
195
- while (true) {
196
- input.signal.throwIfAborted();
197
- const current = await dependencies.api.getBatchStatus({
198
- assetId: latest.asset_id,
199
- fallback: latest,
200
- access: input.access,
201
- signal: input.signal
202
- });
203
- if (current !== null)
204
- latest = current;
205
- if (terminalAnalysisStatus(latest.analysis_status))
206
- return latest;
207
- const remaining = deadline - now().getTime();
208
- if (remaining <= 0) {
209
- dependencies.warn("素材分析仍在处理中,已返回当前结果。");
210
- return latest;
215
+ return stage("wait", async () => {
216
+ const deadline = now().getTime() + 180_000;
217
+ while (true) {
218
+ input.signal.throwIfAborted();
219
+ const current = await dependencies.api.getBatchStatus({
220
+ assetId: latest.asset_id,
221
+ fallback: latest,
222
+ access: input.access,
223
+ signal: input.signal
224
+ });
225
+ if (current !== null)
226
+ latest = current;
227
+ if (terminalAnalysisStatus(latest.analysis_status))
228
+ return latest;
229
+ const remaining = deadline - now().getTime();
230
+ if (remaining <= 0) {
231
+ dependencies.warn("素材分析仍在处理中,已返回当前结果。");
232
+ return latest;
233
+ }
234
+ await sleep(Math.min(3000, remaining), input.signal);
211
235
  }
212
- await sleep(Math.min(3000, remaining), input.signal);
213
- }
236
+ });
214
237
  }
215
238
  catch (error) {
216
239
  if (input.signal.aborted)
@@ -62,57 +62,63 @@ export function createDamUseCases(dependencies) {
62
62
  async list({ input, access, signal }) {
63
63
  signal.throwIfAborted();
64
64
  assertPageSize(input.pageSize);
65
- const repositories = await resolveRepositories(input, access, signal);
66
- const result = await dependencies.api.search({
67
- ...searchRequest(input, "", repositories.map((repository) => repository.id)),
68
- access,
69
- signal
65
+ const repositories = await dependencies.telemetry.stage("repository", () => (resolveRepositories(input, access, signal)));
66
+ return dependencies.telemetry.stage("request", async () => {
67
+ const result = await dependencies.api.search({
68
+ ...searchRequest(input, "", repositories.map((repository) => repository.id)),
69
+ access,
70
+ signal
71
+ });
72
+ signal.throwIfAborted();
73
+ validate.list(result);
74
+ return result;
70
75
  });
71
- signal.throwIfAborted();
72
- validate.list(result);
73
- return result;
74
76
  },
75
77
  async search({ input, access, signal }) {
76
78
  signal.throwIfAborted();
77
79
  if (!nonblank(input.query))
78
80
  invalid();
79
81
  assertPageSize(input.pageSize);
80
- const repositories = await resolveRepositories(input, access, signal);
81
- const result = await dependencies.api.search({
82
- ...searchRequest(input, input.query, repositories.map((repository) => repository.id)),
83
- access,
84
- signal
82
+ const repositories = await dependencies.telemetry.stage("repository", () => (resolveRepositories(input, access, signal)));
83
+ return dependencies.telemetry.stage("request", async () => {
84
+ const result = await dependencies.api.search({
85
+ ...searchRequest(input, input.query, repositories.map((repository) => repository.id)),
86
+ access,
87
+ signal
88
+ });
89
+ signal.throwIfAborted();
90
+ validate.search(result);
91
+ return result;
85
92
  });
86
- signal.throwIfAborted();
87
- validate.search(result);
88
- return result;
89
93
  },
90
94
  async get({ input, access, signal }) {
91
95
  signal.throwIfAborted();
92
96
  assertAssetId(input.asset_id);
93
- const result = await dependencies.api.getAsset({
94
- assetId: input.asset_id,
95
- access,
96
- signal
97
+ return dependencies.telemetry.stage("request", async () => {
98
+ const result = await dependencies.api.getAsset({
99
+ assetId: input.asset_id,
100
+ access,
101
+ signal
102
+ });
103
+ signal.throwIfAborted();
104
+ validate.get(result);
105
+ return result;
97
106
  });
98
- signal.throwIfAborted();
99
- validate.get(result);
100
- return result;
101
107
  },
102
108
  async upload({ input, access, signal }) {
103
109
  signal.throwIfAborted();
104
110
  if (!nonblank(input.file))
105
111
  invalid();
106
- const file = await dependencies.storage.inspect({
112
+ const file = await dependencies.telemetry.stage("inspect", () => (dependencies.storage.inspect({
107
113
  url: pathToFileURL(resolve(process.cwd(), input.file)).href,
108
114
  ...(input.allowSensitivePath === undefined
109
115
  ? {}
110
116
  : { allowSensitivePath: input.allowSensitivePath })
111
- }, signal);
117
+ }, signal)));
112
118
  signal.throwIfAborted();
113
- const [repository] = await resolveRepositories({
119
+ const [repository] = await dependencies.telemetry.stage("repository", () => (resolveRepositories({
114
120
  ...(input.repositoryId === undefined ? {} : { repositoryId: input.repositoryId })
115
- }, access, signal);
121
+ }, access, signal)));
116
122
  if (repository === undefined)
117
123
  throw new RemoteRequestError();
118
124
  const result = await dependencies.uploader.upload({
@@ -129,9 +135,9 @@ export function createDamUseCases(dependencies) {
129
135
  async delete({ input, access, signal }) {
130
136
  signal.throwIfAborted();
131
137
  assertAssetId(input.asset_id);
132
- const [repository] = await resolveRepositories({
138
+ const [repository] = await dependencies.telemetry.stage("repository", () => (resolveRepositories({
133
139
  ...(input.repositoryId === undefined ? {} : { repositoryId: input.repositoryId })
134
- }, access, signal);
140
+ }, access, signal)));
135
141
  if (repository === undefined)
136
142
  throw new RemoteRequestError();
137
143
  const request = {
@@ -140,10 +146,11 @@ export function createDamUseCases(dependencies) {
140
146
  access,
141
147
  signal
142
148
  };
143
- await dependencies.api.recycle(request);
149
+ await dependencies.telemetry.stage("recycle", () => dependencies.api.recycle(request));
144
150
  signal.throwIfAborted();
145
- if (input.permanent === true)
146
- await dependencies.api.deleteRecycled(request);
151
+ if (input.permanent === true) {
152
+ await dependencies.telemetry.stage("delete", () => dependencies.api.deleteRecycled(request));
153
+ }
147
154
  signal.throwIfAborted();
148
155
  validate.delete(null);
149
156
  }
@@ -38,7 +38,11 @@ export function createOrgUseCases(dependencies) {
38
38
  }
39
39
  return {
40
40
  async list(input) {
41
- const records = await dependencies.org.list(input.state.credential, input.signal);
41
+ dependencies.telemetry.annotate({
42
+ account_id: input.state.account?.id,
43
+ organization_id: input.state.organization?.id
44
+ });
45
+ const records = await dependencies.telemetry.stage("list", () => dependencies.org.list(input.state.credential, input.signal));
42
46
  return {
43
47
  orgs: records.map((record) => ({
44
48
  id: record.id,
@@ -53,7 +57,13 @@ export function createOrgUseCases(dependencies) {
53
57
  };
54
58
  },
55
59
  async current() {
56
- const state = await dependencies.store.read();
60
+ const state = await dependencies.telemetry.stage("state", () => dependencies.store.read());
61
+ if (state) {
62
+ dependencies.telemetry.annotate({
63
+ account_id: state.account?.id,
64
+ organization_id: state.organization?.id
65
+ });
66
+ }
57
67
  return {
58
68
  organization: state?.organization
59
69
  ? { id: state.organization.id, name: state.organization.name }
@@ -65,15 +75,21 @@ export function createOrgUseCases(dependencies) {
65
75
  },
66
76
  async switchOrganization(input) {
67
77
  input.signal.throwIfAborted();
68
- const records = await dependencies.org.list(input.state.credential, input.signal);
69
- const selectedId = input.organizationId === undefined
70
- ? await selectOrganization(records, input.select)
71
- : requiredSelection(input.organizationId);
72
- const record = records.find((candidate) => candidate.id === selectedId);
73
- if (!record)
74
- throw new OrganizationInvalidError();
75
- const organization = await bind(input.state.credential, record, input.signal);
76
- await dependencies.store.write({ ...input.state, organization });
78
+ dependencies.telemetry.annotate({ account_id: input.state.account?.id });
79
+ const records = await dependencies.telemetry.stage("list", () => dependencies.org.list(input.state.credential, input.signal));
80
+ const { record, selectedId } = await dependencies.telemetry.stage("select", async () => {
81
+ const selectedId = input.organizationId === undefined
82
+ ? await selectOrganization(records, input.select)
83
+ : requiredSelection(input.organizationId);
84
+ const record = records.find((candidate) => candidate.id === selectedId);
85
+ if (!record)
86
+ throw new OrganizationInvalidError();
87
+ return { record, selectedId };
88
+ });
89
+ dependencies.telemetry.annotate({ parameters: { organizationId: selectedId } });
90
+ const organization = await dependencies.telemetry.stage("bind", () => bind(input.state.credential, record, input.signal));
91
+ await dependencies.telemetry.stage("state", () => dependencies.store.write({ ...input.state, organization }));
92
+ dependencies.telemetry.annotate({ organization_id: organization.id });
77
93
  return record;
78
94
  },
79
95
  async bindAuthorizedOrganization(input) {
@@ -56,10 +56,14 @@ export function projectModelList(catalog, tool) {
56
56
  .map(summary)
57
57
  };
58
58
  }
59
- export function projectModelDetail(catalog, model) {
59
+ export function findModel(catalog, model) {
60
60
  const found = catalog.models.find((candidate) => candidate.model === model);
61
61
  if (found === undefined)
62
62
  throw new ToolInputError();
63
+ return found;
64
+ }
65
+ export function projectModelDetail(catalog, model) {
66
+ const found = findModel(catalog, model);
63
67
  return {
64
68
  ...summary(found),
65
69
  ...(found.usageDescription === undefined
@@ -72,8 +76,8 @@ export function projectModelDetail(catalog, model) {
72
76
  };
73
77
  }
74
78
  export function findToolModel(catalog, tool, model) {
75
- const found = catalog.models.find((candidate) => candidate.tool === tool && candidate.model === model);
76
- if (found === undefined)
79
+ const found = findModel(catalog, model);
80
+ if (found.tool !== tool)
77
81
  throw new ToolInputError();
78
82
  return found;
79
83
  }