@zalosai/zalos-cli 0.2.16 → 0.2.18
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/PUBLISHING.md +3 -0
- package/README.md +3 -1
- package/dist/client.js +2 -2
- package/dist/commands/close.js +89 -0
- package/dist/commands/data-table.js +3 -1
- package/dist/commands/desktop-metrics.js +48 -0
- package/dist/commands/desktop.js +62 -0
- package/dist/commands/execution.js +12 -1
- package/dist/commands/file.js +61 -2
- package/dist/commands/skills.js +10 -2
- package/dist/main.js +4 -0
- package/dist/utils/desktop-update.js +179 -0
- package/dist/utils/developer-db.js +6 -0
- package/package.json +4 -3
- package/skills/custom-page-authoring/SKILL.md +2 -1
- package/skills/update-zalos/SKILL.md +18 -0
- package/skills/zl-ops/SKILL.md +23 -0
package/PUBLISHING.md
CHANGED
|
@@ -59,6 +59,9 @@ Do not publish with uncommitted package changes or reuse an existing version.
|
|
|
59
59
|
|
|
60
60
|
## Central developer access release
|
|
61
61
|
|
|
62
|
+
Version 0.2.17 bundles Custom Page authoring guidance for team-local navigation and self-contained
|
|
63
|
+
downloads. Custom Page navigation never starts workflows, and downloads are limited to supplied
|
|
64
|
+
in-memory content.
|
|
62
65
|
Version 0.2.15 bundles the `zl-ops` production-operations skill and adds offline `zl skills`
|
|
63
66
|
commands to list, read, and install it into Codex or Claude skill roots.
|
|
64
67
|
Version 0.2.16 adds `zl execution replay export`, which materializes a SHA-256-verified local
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Requires Node.js 22 or newer. The product CLI is public on npm and does not
|
|
|
11
11
|
require GitHub CLI or GitHub Packages access.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install --global @zalosai/zalos-cli@0.2.
|
|
14
|
+
npm install --global @zalosai/zalos-cli@0.2.18
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Installing the CLI does not grant access.
|
|
@@ -135,6 +135,8 @@ zl file snapshot --team-id <team-ref> --output /tmp/team-files.json
|
|
|
135
135
|
zl file seed --source-team-id <team-ref> --source-path /Fixtures --target-path /Rehearsal
|
|
136
136
|
zl file seed --source-team-id <team-ref> --source-scope generated-outputs --source-path /__mesh/outputs/<execution-id> --target-path /Priors
|
|
137
137
|
zl file seed --source-team-id <team-ref> --source-path /Fixtures --target-path /Rehearsal --apply --expected-checksum <sha256>
|
|
138
|
+
zl close registry apply --file <overlay.yaml> --team-id <team-ref> --catalog <path-or-url> [--revision <sha>] --dry-run
|
|
139
|
+
zl close registry show --team-id <team-ref>
|
|
138
140
|
```
|
|
139
141
|
|
|
140
142
|
Workflow import/update/publish require `--apply`; without it they validate and print a preview.
|
package/dist/client.js
CHANGED
|
@@ -40,7 +40,7 @@ async function request(options, tokenOverride) {
|
|
|
40
40
|
headers['Content-Type'] = 'application/json';
|
|
41
41
|
const response = await fetch(url.toString(), {
|
|
42
42
|
method,
|
|
43
|
-
signal: AbortSignal.timeout(30_000),
|
|
43
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),
|
|
44
44
|
redirect: 'error',
|
|
45
45
|
headers,
|
|
46
46
|
body: body != null ? (isFormData(body) ? body : JSON.stringify(body)) : undefined,
|
|
@@ -68,7 +68,7 @@ async function request(options, tokenOverride) {
|
|
|
68
68
|
export const client = {
|
|
69
69
|
get: (path, query) => request({ method: 'GET', path, query }),
|
|
70
70
|
post: (path, body, options) => request({ method: 'POST', path, body, ...options }),
|
|
71
|
-
postForm: (path, body) => request({ method: 'POST', path, body }),
|
|
71
|
+
postForm: (path, body, options) => request({ method: 'POST', path, body, ...options }),
|
|
72
72
|
put: (path, body) => request({ method: 'PUT', path, body }),
|
|
73
73
|
patch: (path, body) => request({ method: 'PATCH', path, body }),
|
|
74
74
|
delete: (path) => request({ method: 'DELETE', path }),
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { parse as parseYaml } from 'yaml';
|
|
3
|
+
import { client } from '../client.js';
|
|
4
|
+
import { currentEnvironment } from '../environments.js';
|
|
5
|
+
import { ValidationError } from '../utils/errors.js';
|
|
6
|
+
import { printJson } from '../utils/output.js';
|
|
7
|
+
import { action, dataRow, isRow, resourceId } from '../utils/product-api.js';
|
|
8
|
+
const APPLY_TIMEOUT_MS = 120_000;
|
|
9
|
+
async function readCatalogJson(location) {
|
|
10
|
+
let text;
|
|
11
|
+
if (/^https?:\/\//.test(location)) {
|
|
12
|
+
const response = await fetch(location, { signal: AbortSignal.timeout(APPLY_TIMEOUT_MS) });
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw new ValidationError('catalog', `Catalog fetch failed: ${response.status} ${location}`);
|
|
15
|
+
}
|
|
16
|
+
text = await response.text();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
text = await readFile(location, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new ValidationError('catalog', 'Catalog is not valid JSON');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Applies a client's finance registry overlay (D18) to a team. With `--catalog` the catalog file
|
|
30
|
+
* is read here and sent; without it the server fetches the catalog the overlay pins.
|
|
31
|
+
*/
|
|
32
|
+
export async function applyRegistryAction(options) {
|
|
33
|
+
const overlayText = await readFile(options.file, 'utf8');
|
|
34
|
+
let overlay;
|
|
35
|
+
try {
|
|
36
|
+
overlay = parseYaml(overlayText);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
throw new ValidationError('file', `Overlay is not valid YAML: ${error instanceof Error ? error.message : String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
if (!isRow(overlay))
|
|
42
|
+
throw new ValidationError('file', 'Overlay must be a YAML mapping');
|
|
43
|
+
const payload = {
|
|
44
|
+
teamId: resourceId(options.teamId),
|
|
45
|
+
environment: currentEnvironment().name,
|
|
46
|
+
overlay,
|
|
47
|
+
dryRun: Boolean(options.dryRun),
|
|
48
|
+
};
|
|
49
|
+
if (options.revision)
|
|
50
|
+
payload.revision = options.revision;
|
|
51
|
+
if (options.catalog) {
|
|
52
|
+
payload.catalog = await readCatalogJson(options.catalog);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
if (!isRow(overlay.catalog)) {
|
|
56
|
+
throw new ValidationError('catalog', 'Pass --catalog, or pin catalog in the overlay');
|
|
57
|
+
}
|
|
58
|
+
// Only where the catalog lives; `catalog.client` stays in the overlay, which the server reads.
|
|
59
|
+
const { repository, revision, path } = overlay.catalog;
|
|
60
|
+
payload.catalogRef = { repository, revision: options.revision ?? revision, path };
|
|
61
|
+
}
|
|
62
|
+
printJson(dataRow(await client.post('/developer/finance/registry/apply', payload, {
|
|
63
|
+
timeoutMs: APPLY_TIMEOUT_MS,
|
|
64
|
+
})));
|
|
65
|
+
}
|
|
66
|
+
export async function showRegistryAction(options) {
|
|
67
|
+
printJson(dataRow(await client.get('/developer/finance/registry', { teamId: resourceId(options.teamId) })));
|
|
68
|
+
}
|
|
69
|
+
export function registerCloseCommand(program) {
|
|
70
|
+
const close = program.command('close').description('Month-end close operations');
|
|
71
|
+
const registry = close
|
|
72
|
+
.command('registry')
|
|
73
|
+
.description("A client's finance registry: sources, datasets, agents and close tasks");
|
|
74
|
+
registry
|
|
75
|
+
.command('apply')
|
|
76
|
+
.description('Apply a client overlay and the agent-studio catalog to a team; prints the diff')
|
|
77
|
+
.requiredOption('--file <overlay.yaml>', 'Client overlay YAML')
|
|
78
|
+
.requiredOption('--team-id <ref>', 'Target team public ID (preferred) or UUID')
|
|
79
|
+
.option('--catalog <path-or-url>', 'Catalog JSON; default: the catalog the overlay pins')
|
|
80
|
+
.option('--revision <sha>', 'Catalog revision to pin; default: the overlay pin')
|
|
81
|
+
.option('--dry-run', 'Print the diff without writing anything')
|
|
82
|
+
.action(async (o) => action(() => applyRegistryAction(o)));
|
|
83
|
+
registry
|
|
84
|
+
.command('show')
|
|
85
|
+
.description("Print a team's registry as applied, with its latest version")
|
|
86
|
+
.requiredOption('--team-id <ref>', 'Team public ID (preferred) or UUID')
|
|
87
|
+
.action(async (o) => action(() => showRegistryAction(o)));
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=close.js.map
|
|
@@ -98,11 +98,12 @@ export async function writerAction(tableId, workflowId, grant, options) {
|
|
|
98
98
|
method,
|
|
99
99
|
path,
|
|
100
100
|
...(grant ? { workflowId: resourceId(workflowId) } : {}),
|
|
101
|
+
...(grant && options.includeRead ? { includeRead: true } : {}),
|
|
101
102
|
});
|
|
102
103
|
return;
|
|
103
104
|
}
|
|
104
105
|
if (grant)
|
|
105
|
-
await client.post(path, { workflowId: resourceId(workflowId) }, { detailedAuthErrors: true });
|
|
106
|
+
await client.post(path, { workflowId: resourceId(workflowId), ...(options.includeRead ? { includeRead: true } : {}) }, { detailedAuthErrors: true });
|
|
106
107
|
else
|
|
107
108
|
await client.delete(path);
|
|
108
109
|
printJson({ tableId, workflowId, granted: grant });
|
|
@@ -134,6 +135,7 @@ export function registerDataTableCommand(program) {
|
|
|
134
135
|
.action(async (id) => action(() => viewTableAction(id, fields(program))));
|
|
135
136
|
cmd
|
|
136
137
|
.command('grant-writer <table-id> <workflow-id>')
|
|
138
|
+
.option('--include-read', 'Also grant lookup access to this table')
|
|
137
139
|
.option('--apply', 'Grant writer permission')
|
|
138
140
|
.option('--dry-run', 'Preview grant (default)')
|
|
139
141
|
.action(async (tableId, workflowId, options) => action(() => writerAction(tableId, workflowId, true, options)));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { client } from '../client.js';
|
|
2
|
+
import { action } from '../utils/product-api.js';
|
|
3
|
+
import { printJson } from '../utils/output.js';
|
|
4
|
+
import { ValidationError } from '../utils/errors.js';
|
|
5
|
+
export function metricsQuery(options, now = new Date()) {
|
|
6
|
+
const since = options.since ?? '24h';
|
|
7
|
+
const relative = /^(\d+)(h|d)$/.exec(since);
|
|
8
|
+
const start = relative
|
|
9
|
+
? new Date(now.getTime() - Number(relative[1]) * (relative[2] === 'h' ? 3600000 : 86400000))
|
|
10
|
+
: new Date(since);
|
|
11
|
+
const end = options.until ? new Date(options.until) : now;
|
|
12
|
+
const limit = Number(options.limit ?? 100);
|
|
13
|
+
if (!Number.isFinite(start.getTime()) ||
|
|
14
|
+
!Number.isFinite(end.getTime()) ||
|
|
15
|
+
start >= end ||
|
|
16
|
+
!Number.isInteger(limit) ||
|
|
17
|
+
limit < 1 ||
|
|
18
|
+
limit > 100)
|
|
19
|
+
throw new ValidationError('query', 'Use valid dates or --since 24h, and a limit from 1 to 100');
|
|
20
|
+
const query = new URLSearchParams({
|
|
21
|
+
since: start.toISOString(),
|
|
22
|
+
until: end.toISOString(),
|
|
23
|
+
limit: String(limit),
|
|
24
|
+
});
|
|
25
|
+
for (const [key, value] of [
|
|
26
|
+
['snapshot', options.snapshot],
|
|
27
|
+
['userId', options.user],
|
|
28
|
+
['cursor', options.cursor],
|
|
29
|
+
])
|
|
30
|
+
if (value)
|
|
31
|
+
query.set(key, value);
|
|
32
|
+
return query.toString();
|
|
33
|
+
}
|
|
34
|
+
export function registerDesktopMetricsCommand(desktop) {
|
|
35
|
+
desktop
|
|
36
|
+
.command('metrics')
|
|
37
|
+
.description('Read content-free desktop metrics for the current team (approved reviewers only)')
|
|
38
|
+
.option('--since <time>', 'ISO timestamp or lookback such as 24h or 7d', '24h')
|
|
39
|
+
.option('--until <time>', 'Exclusive ISO end; reuse the returned bounds for subsequent pages')
|
|
40
|
+
.option('--snapshot <time>', 'Reuse the returned snapshot for subsequent pages')
|
|
41
|
+
.option('--user <id>', 'Filter one user')
|
|
42
|
+
.option('--cursor <cursor>', 'Continue with the returned nextCursor and original since/until/snapshot')
|
|
43
|
+
.option('--limit <count>', 'Reporting packets per page (1–100)', '100')
|
|
44
|
+
.action(async (options) => action(async () => {
|
|
45
|
+
printJson(await client.get(`/desktop/metrics?${metricsQuery(options)}`));
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=desktop-metrics.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { registerDesktopMetricsCommand } from './desktop-metrics.js';
|
|
2
|
+
import { client } from '../client.js';
|
|
3
|
+
import { currentEnvironment } from '../environments.js';
|
|
4
|
+
import { action } from '../utils/product-api.js';
|
|
5
|
+
import { printJson } from '../utils/output.js';
|
|
6
|
+
import { ValidationError } from '../utils/errors.js';
|
|
7
|
+
import { compareVersions, compatibleMacOS, defaultInstallerPath, downloadInstaller, installedDesktop, parseRelease, } from '../utils/desktop-update.js';
|
|
8
|
+
export async function updateDesktop(options) {
|
|
9
|
+
const environment = currentEnvironment().name;
|
|
10
|
+
const installed = await installedDesktop(environment, options.app);
|
|
11
|
+
const release = parseRelease(await client.get('/desktop/release'));
|
|
12
|
+
const base = { environment, installedVersion: installed.version, app: installed.path };
|
|
13
|
+
if (!release)
|
|
14
|
+
return {
|
|
15
|
+
...base,
|
|
16
|
+
status: 'unpublished',
|
|
17
|
+
message: 'No desktop release is available in this environment yet.',
|
|
18
|
+
};
|
|
19
|
+
const order = compareVersions(release.version, installed.version);
|
|
20
|
+
const compatible = compatibleMacOS(installed.macOS, release.minimumMacOS);
|
|
21
|
+
const summary = {
|
|
22
|
+
...base,
|
|
23
|
+
latestVersion: release.version,
|
|
24
|
+
updateAvailable: order > 0,
|
|
25
|
+
compatible,
|
|
26
|
+
minimumMacOS: release.minimumMacOS,
|
|
27
|
+
};
|
|
28
|
+
if (options.check || order <= 0)
|
|
29
|
+
return {
|
|
30
|
+
...summary,
|
|
31
|
+
status: order > 0 ? 'available' : order === 0 ? 'up-to-date' : 'installed-newer',
|
|
32
|
+
};
|
|
33
|
+
if (!compatible)
|
|
34
|
+
throw new ValidationError('macOS', `This release requires macOS ${release.minimumMacOS} or later`);
|
|
35
|
+
const path = await downloadInstaller(release, options.output ?? defaultInstallerPath(release, environment));
|
|
36
|
+
return {
|
|
37
|
+
...summary,
|
|
38
|
+
status: 'downloaded',
|
|
39
|
+
path,
|
|
40
|
+
sha256: release.sha256,
|
|
41
|
+
installed: false,
|
|
42
|
+
nextSteps: [
|
|
43
|
+
'Finish any running tasks and save your work.',
|
|
44
|
+
'Quit Zalos. This chat will stop when the app quits.',
|
|
45
|
+
`Open the downloaded DMG and replace ${installed.path} with the matching Zalos app.`,
|
|
46
|
+
'Reopen Zalos. If macOS blocks the unsigned preview, use System Settings > Privacy & Security > Open Anyway.',
|
|
47
|
+
'Run the update check again to verify the installed version.',
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function registerDesktopCommand(program) {
|
|
52
|
+
const desktop = program.command('desktop').description('Manage the Zalos Desktop application');
|
|
53
|
+
desktop
|
|
54
|
+
.command('update')
|
|
55
|
+
.description('Check or download a verified desktop installer; installation remains manual')
|
|
56
|
+
.option('--check', 'Check for a newer release without downloading')
|
|
57
|
+
.option('--app <path>', 'Installed Zalos .app path')
|
|
58
|
+
.option('--output <path>', 'Installer destination; existing files are never overwritten')
|
|
59
|
+
.action(async (options) => action(async () => printJson(await updateDesktop(options))));
|
|
60
|
+
registerDesktopMetricsCommand(desktop);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=desktop.js.map
|
|
@@ -184,8 +184,15 @@ export async function listExecutionsAction(options) {
|
|
|
184
184
|
export async function viewExecutionAction(id, options, summary = false) {
|
|
185
185
|
printJson(filterSingle(dataRow(await client.get(`/executions/${resourceId(id)}${summary ? '/summary' : ''}`)), options.fields));
|
|
186
186
|
}
|
|
187
|
+
export async function stopExecutionAction(id) {
|
|
188
|
+
const executionId = resourceId(id);
|
|
189
|
+
const result = await client.post(`/executions/${executionId}/stop`);
|
|
190
|
+
if (!isRow(result) || result.stopped !== true)
|
|
191
|
+
throw new ApiError(502, 'Invalid execution stop response');
|
|
192
|
+
printJson({ executionId, stopped: true });
|
|
193
|
+
}
|
|
187
194
|
export function registerExecutionCommand(program) {
|
|
188
|
-
const cmd = program.command('execution').description('Inspect workflow executions');
|
|
195
|
+
const cmd = program.command('execution').description('Inspect and control workflow executions');
|
|
189
196
|
cmd
|
|
190
197
|
.command('recovery')
|
|
191
198
|
.description('Owner/internal recovery evidence')
|
|
@@ -205,6 +212,10 @@ export function registerExecutionCommand(program) {
|
|
|
205
212
|
cmd
|
|
206
213
|
.command(`${verb} <id>`)
|
|
207
214
|
.action(async (id) => action(() => viewExecutionAction(id, fields(program), verb === 'summary')));
|
|
215
|
+
cmd
|
|
216
|
+
.command('stop <execution-id>')
|
|
217
|
+
.description('Stop a running execution and its downstream work')
|
|
218
|
+
.action(async (id) => action(() => stopExecutionAction(id)));
|
|
208
219
|
cmd
|
|
209
220
|
.command('replay')
|
|
210
221
|
.description('Materialize verified local replay bundles')
|
package/dist/commands/file.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { openAsBlob } from 'node:fs';
|
|
2
|
+
import { stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, extname } from 'node:path';
|
|
2
4
|
import { client } from '../client.js';
|
|
3
5
|
import { currentEnvironment } from '../environments.js';
|
|
4
6
|
import { ValidationError } from '../utils/errors.js';
|
|
@@ -6,6 +8,25 @@ import { downloadToNewFile } from '../utils/download-file.js';
|
|
|
6
8
|
import { filterSingle, printJson } from '../utils/output.js';
|
|
7
9
|
import { action, dataRow, dataRows, fields, isRow, printPage, resourceId, } from '../utils/product-api.js';
|
|
8
10
|
const FILE_FIELDS = 'id,name,fileName,mimeType,size,fileSize,folderId,createdAt,updatedAt';
|
|
11
|
+
const MAX_FILE_SIZE_BYTES = 500 * 1024 * 1024;
|
|
12
|
+
const UPLOAD_TIMEOUT_MS = 15 * 60_000;
|
|
13
|
+
const MIME_TYPES = {
|
|
14
|
+
'.csv': 'text/csv',
|
|
15
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
16
|
+
'.gif': 'image/gif',
|
|
17
|
+
'.html': 'text/html',
|
|
18
|
+
'.jpeg': 'image/jpeg',
|
|
19
|
+
'.jpg': 'image/jpeg',
|
|
20
|
+
'.json': 'application/json',
|
|
21
|
+
'.md': 'text/markdown',
|
|
22
|
+
'.pdf': 'application/pdf',
|
|
23
|
+
'.png': 'image/png',
|
|
24
|
+
'.txt': 'text/plain',
|
|
25
|
+
'.webp': 'image/webp',
|
|
26
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
27
|
+
'.xml': 'application/xml',
|
|
28
|
+
'.zip': 'application/zip',
|
|
29
|
+
};
|
|
9
30
|
async function files(options) {
|
|
10
31
|
return dataRows(await client.get('/team-files', {
|
|
11
32
|
folderId: options.folderId ? resourceId(options.folderId) : 'root',
|
|
@@ -70,6 +91,37 @@ export async function downloadFileAction(idOrPath, options) {
|
|
|
70
91
|
allowDataUrl: options.allowDataUrl,
|
|
71
92
|
}));
|
|
72
93
|
}
|
|
94
|
+
export async function uploadFileAction(localPath, options) {
|
|
95
|
+
const folderId = options.folderId ? resourceId(options.folderId) : 'root';
|
|
96
|
+
const mimeType = options.mimeType ?? MIME_TYPES[extname(localPath).toLowerCase()] ?? 'application/octet-stream';
|
|
97
|
+
if (!/^[\w.+-]+\/[\w.+-]+$/.test(mimeType)) {
|
|
98
|
+
throw new ValidationError('mimeType', 'Invalid MIME type');
|
|
99
|
+
}
|
|
100
|
+
let fileStat;
|
|
101
|
+
try {
|
|
102
|
+
fileStat = await stat(localPath);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new ValidationError('file', `Unable to read local file: ${localPath}`);
|
|
106
|
+
}
|
|
107
|
+
if (!fileStat.isFile())
|
|
108
|
+
throw new ValidationError('file', 'Local path must be a regular file');
|
|
109
|
+
if (fileStat.size > MAX_FILE_SIZE_BYTES) {
|
|
110
|
+
throw new ValidationError('file', 'File exceeds the 500 MiB Team Files limit');
|
|
111
|
+
}
|
|
112
|
+
let file;
|
|
113
|
+
try {
|
|
114
|
+
file = await openAsBlob(localPath, { type: mimeType });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new ValidationError('file', `Unable to read local file: ${localPath}`);
|
|
118
|
+
}
|
|
119
|
+
const form = new FormData();
|
|
120
|
+
form.append('folderId', folderId);
|
|
121
|
+
form.append('collisionStrategy', options.overwrite ? 'overwrite' : 'create_unique_name');
|
|
122
|
+
form.append('file', file, basename(localPath));
|
|
123
|
+
printJson(dataRow(await client.postForm('/team-files/upload', form, { timeoutMs: UPLOAD_TIMEOUT_MS })));
|
|
124
|
+
}
|
|
73
125
|
export async function snapshotFilesAction(options) {
|
|
74
126
|
if (options.teamId) {
|
|
75
127
|
const snapshot = {
|
|
@@ -179,7 +231,7 @@ export async function moveFileAction(id, options) {
|
|
|
179
231
|
printJson(filterSingle(dataRow(await client.patch(path, payload)), FILE_FIELDS));
|
|
180
232
|
}
|
|
181
233
|
export function registerFileCommand(program) {
|
|
182
|
-
const cmd = program.command('file').description('
|
|
234
|
+
const cmd = program.command('file').description('Manage Team Files');
|
|
183
235
|
cmd
|
|
184
236
|
.command('list')
|
|
185
237
|
.option('--folder-id <id>', 'Folder ID (defaults to root)')
|
|
@@ -223,6 +275,13 @@ export function registerFileCommand(program) {
|
|
|
223
275
|
allowDataUrl: ['localhost', '127.0.0.1', '[::1]'].includes(hostname),
|
|
224
276
|
});
|
|
225
277
|
}));
|
|
278
|
+
cmd
|
|
279
|
+
.command('upload <local-path>')
|
|
280
|
+
.description('Upload a local file to the selected team')
|
|
281
|
+
.option('--folder-id <id>', 'Destination folder ID (defaults to root)')
|
|
282
|
+
.option('--mime-type <type>', 'Override the detected MIME type')
|
|
283
|
+
.option('--overwrite', 'Replace an existing file with the same name')
|
|
284
|
+
.action(async (path, o) => action(() => uploadFileAction(path, o)));
|
|
226
285
|
cmd
|
|
227
286
|
.command('snapshot')
|
|
228
287
|
.description('Snapshot metadata within a folder')
|
package/dist/commands/skills.js
CHANGED
|
@@ -11,8 +11,10 @@ function isSkillTarget(value) {
|
|
|
11
11
|
return value === 'claude' || value === 'codex';
|
|
12
12
|
}
|
|
13
13
|
function skillPath(name) {
|
|
14
|
+
if (name === 'update-zalos')
|
|
15
|
+
return fileURLToPath(new URL('../../skills/update-zalos/SKILL.md', import.meta.url));
|
|
14
16
|
if (name !== OPS_SKILL_NAME)
|
|
15
|
-
throw new ValidationError('skill', `Unknown skill: ${name}. Available: ${OPS_SKILL_NAME}`);
|
|
17
|
+
throw new ValidationError('skill', `Unknown skill: ${name}. Available: ${OPS_SKILL_NAME}, update-zalos`);
|
|
16
18
|
return OPS_SKILL_PATH;
|
|
17
19
|
}
|
|
18
20
|
export function resolveDefaultSkillsDirectory(target, home = homedir()) {
|
|
@@ -71,7 +73,13 @@ export function registerSkillsCommand(program) {
|
|
|
71
73
|
.command('list')
|
|
72
74
|
.description('List bundled skills (no network or authentication required)')
|
|
73
75
|
.action(() => printJson({
|
|
74
|
-
skills: [
|
|
76
|
+
skills: [
|
|
77
|
+
{ name: OPS_SKILL_NAME, description: 'Zalos production operations' },
|
|
78
|
+
{
|
|
79
|
+
name: 'update-zalos',
|
|
80
|
+
description: 'Check and download verified Zalos Desktop updates',
|
|
81
|
+
},
|
|
82
|
+
],
|
|
75
83
|
}));
|
|
76
84
|
command
|
|
77
85
|
.command('read <name>')
|
package/dist/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { registerDesktopCommand } from './commands/desktop.js';
|
|
4
5
|
import { registerSetupCommand } from './commands/setup.js';
|
|
5
6
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
6
7
|
import { registerWorkflowCommand } from './commands/workflow.js';
|
|
@@ -12,6 +13,7 @@ import { registerFileCommand } from './commands/file.js';
|
|
|
12
13
|
import { registerAuthCommand } from './commands/auth.js';
|
|
13
14
|
import { registerApiTokenCommand } from './commands/api-token.js';
|
|
14
15
|
import { registerCycleCommand } from './commands/cycle.js';
|
|
16
|
+
import { registerCloseCommand } from './commands/close.js';
|
|
15
17
|
import { registerLabelCommand } from './commands/label.js';
|
|
16
18
|
import { registerMemberCommand } from './commands/member.js';
|
|
17
19
|
import { registerProjectCommand } from './commands/project.js';
|
|
@@ -62,6 +64,7 @@ program
|
|
|
62
64
|
program.action(() => {
|
|
63
65
|
program.help();
|
|
64
66
|
});
|
|
67
|
+
registerDesktopCommand(program);
|
|
65
68
|
registerAuthCommand(program);
|
|
66
69
|
registerApiTokenCommand(program);
|
|
67
70
|
registerSetupCommand(program);
|
|
@@ -80,6 +83,7 @@ registerSchemaCommand(program);
|
|
|
80
83
|
registerStateCommand(program);
|
|
81
84
|
registerLabelCommand(program);
|
|
82
85
|
registerCycleCommand(program);
|
|
86
|
+
registerCloseCommand(program);
|
|
83
87
|
registerProjectCommand(program);
|
|
84
88
|
registerMemberCommand(program);
|
|
85
89
|
registerTaskCommand(program);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { link, lstat, mkdir, mkdtemp, open, rm } from 'node:fs/promises';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { ValidationError } from './errors.js';
|
|
8
|
+
const run = promisify(execFile);
|
|
9
|
+
const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
10
|
+
function versionParts(value) {
|
|
11
|
+
const match = VERSION.exec(value);
|
|
12
|
+
if (!match || match[4]?.split('.').some((part) => /^0\d+$/.test(part)))
|
|
13
|
+
throw new ValidationError('version', 'Expected a valid release version');
|
|
14
|
+
return { core: match.slice(1, 4).map(BigInt), pre: match[4]?.split('.') ?? [] };
|
|
15
|
+
}
|
|
16
|
+
export function compareVersions(left, right) {
|
|
17
|
+
const a = versionParts(left);
|
|
18
|
+
const b = versionParts(right);
|
|
19
|
+
for (let i = 0; i < 3; i++) {
|
|
20
|
+
if (a.core[i] !== b.core[i])
|
|
21
|
+
return a.core[i] > b.core[i] ? 1 : -1;
|
|
22
|
+
}
|
|
23
|
+
if (!a.pre.length || !b.pre.length)
|
|
24
|
+
return a.pre.length === b.pre.length ? 0 : a.pre.length ? -1 : 1;
|
|
25
|
+
for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
|
|
26
|
+
const x = a.pre[i];
|
|
27
|
+
const y = b.pre[i];
|
|
28
|
+
if (x === y)
|
|
29
|
+
continue;
|
|
30
|
+
if (x === undefined || y === undefined)
|
|
31
|
+
return x === undefined ? -1 : 1;
|
|
32
|
+
const nx = /^\d+$/.test(x);
|
|
33
|
+
const ny = /^\d+$/.test(y);
|
|
34
|
+
if (nx && ny)
|
|
35
|
+
return BigInt(x) > BigInt(y) ? 1 : -1;
|
|
36
|
+
if (nx !== ny)
|
|
37
|
+
return nx ? -1 : 1;
|
|
38
|
+
return x > y ? 1 : -1;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
export function parseRelease(response) {
|
|
43
|
+
const release = response?.data?.release;
|
|
44
|
+
if (release === null)
|
|
45
|
+
return null;
|
|
46
|
+
if (!release || typeof release !== 'object')
|
|
47
|
+
throw new ValidationError('release', 'Invalid release metadata');
|
|
48
|
+
const value = release;
|
|
49
|
+
if (typeof value.version !== 'string' ||
|
|
50
|
+
typeof value.url !== 'string' ||
|
|
51
|
+
typeof value.sha256 !== 'string' ||
|
|
52
|
+
!/^[a-f0-9]{64}$/.test(value.sha256) ||
|
|
53
|
+
value.architecture !== 'arm64' ||
|
|
54
|
+
typeof value.minimumMacOS !== 'string' ||
|
|
55
|
+
!/^\d+\.\d+(?:\.\d+)?$/.test(value.minimumMacOS))
|
|
56
|
+
throw new ValidationError('release', 'Invalid release metadata');
|
|
57
|
+
versionParts(value.version);
|
|
58
|
+
let url;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(value.url);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new ValidationError('release', 'Invalid installer URL');
|
|
64
|
+
}
|
|
65
|
+
if (url.protocol !== 'https:' ||
|
|
66
|
+
url.username ||
|
|
67
|
+
url.password ||
|
|
68
|
+
url.hash ||
|
|
69
|
+
url.port ||
|
|
70
|
+
(!/^[a-z0-9.-]+\.s3\.[a-z0-9-]+\.amazonaws\.com$/.test(url.hostname) &&
|
|
71
|
+
!(value.notarized === true &&
|
|
72
|
+
url.href === value.url &&
|
|
73
|
+
url.origin === 'https://downloads.zalos.ai' &&
|
|
74
|
+
!url.search &&
|
|
75
|
+
/^\/desktop\/mac\/[a-f0-9]{40}\/Zalos-[A-Za-z0-9.-]+-mac-arm64\.dmg$/.test(url.pathname))) ||
|
|
76
|
+
!url.pathname.endsWith('.dmg'))
|
|
77
|
+
throw new ValidationError('release', 'Unsupported installer source');
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
export function validateAppInfo(info, environment) {
|
|
81
|
+
const expectedId = environment === 'staging' ? 'com.zalos.desktop.staging' : 'com.zalos.desktop';
|
|
82
|
+
if (environment === 'eu-prod')
|
|
83
|
+
throw new ValidationError('environment', 'No EU desktop build is configured yet');
|
|
84
|
+
if (info.CFBundleIdentifier !== expectedId)
|
|
85
|
+
throw new ValidationError('app', 'Installed app does not match the selected environment');
|
|
86
|
+
const version = info.CFBundleShortVersionString;
|
|
87
|
+
if (typeof version !== 'string')
|
|
88
|
+
throw new ValidationError('app', 'Installed app version is missing');
|
|
89
|
+
versionParts(version);
|
|
90
|
+
return version;
|
|
91
|
+
}
|
|
92
|
+
export async function installedDesktop(environment, appPath) {
|
|
93
|
+
if (process.platform !== 'darwin' || process.arch !== 'arm64')
|
|
94
|
+
throw new ValidationError('platform', 'The current desktop preview supports Apple Silicon macOS only');
|
|
95
|
+
const bundledApp = process.execPath.match(/^(.*\.app)\/Contents\//)?.[1];
|
|
96
|
+
const app = resolve(appPath ??
|
|
97
|
+
bundledApp ??
|
|
98
|
+
`/Applications/${environment === 'staging' ? 'Zalos Staging' : 'Zalos'}.app`);
|
|
99
|
+
let info;
|
|
100
|
+
try {
|
|
101
|
+
const { stdout } = await run('/usr/bin/plutil', [
|
|
102
|
+
'-convert',
|
|
103
|
+
'json',
|
|
104
|
+
'-o',
|
|
105
|
+
'-',
|
|
106
|
+
join(app, 'Contents/Info.plist'),
|
|
107
|
+
]);
|
|
108
|
+
info = JSON.parse(stdout);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new ValidationError('app', 'Cannot read the installed app; provide --app with its .app path');
|
|
112
|
+
}
|
|
113
|
+
const { stdout } = await run('/usr/bin/sw_vers', ['-productVersion']);
|
|
114
|
+
return { version: validateAppInfo(info, environment), path: app, macOS: stdout.trim() };
|
|
115
|
+
}
|
|
116
|
+
export function compatibleMacOS(current, minimum) {
|
|
117
|
+
const normalize = (value) => value.split('.').length === 2 ? `${value}.0` : value;
|
|
118
|
+
return compareVersions(normalize(current), normalize(minimum)) >= 0;
|
|
119
|
+
}
|
|
120
|
+
/** Uncredentialed artifact request: never forward the Mesh token or print signed URLs. */
|
|
121
|
+
export async function downloadInstaller(release, destination, fetcher = fetch) {
|
|
122
|
+
const output = resolve(destination);
|
|
123
|
+
try {
|
|
124
|
+
await lstat(output);
|
|
125
|
+
throw Object.assign(new Error('Installer destination already exists; choose another --output path'), { code: 'EEXIST' });
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
if (!error || typeof error !== 'object' || !('code' in error) || error.code !== 'ENOENT')
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
await mkdir(dirname(output), { recursive: true });
|
|
132
|
+
const temporary = await mkdtemp(join(dirname(output), '.zalos-update-'));
|
|
133
|
+
const partial = join(temporary, 'installer.part');
|
|
134
|
+
try {
|
|
135
|
+
let response;
|
|
136
|
+
try {
|
|
137
|
+
response = await fetcher(release.url, {
|
|
138
|
+
redirect: 'error',
|
|
139
|
+
signal: AbortSignal.timeout(30 * 60 * 1000),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw new Error('Installer download failed; run the command again for a fresh download link');
|
|
144
|
+
}
|
|
145
|
+
if (!response.ok || !response.body)
|
|
146
|
+
throw new Error(`Installer download failed (HTTP ${response.status})`);
|
|
147
|
+
const hash = createHash('sha256');
|
|
148
|
+
const file = await open(partial, 'wx', 0o600);
|
|
149
|
+
let size = 0;
|
|
150
|
+
try {
|
|
151
|
+
for await (const chunk of response.body) {
|
|
152
|
+
size += chunk.length;
|
|
153
|
+
if (size > 4 * 1024 ** 3)
|
|
154
|
+
throw new Error('Installer exceeds the supported size limit');
|
|
155
|
+
hash.update(chunk);
|
|
156
|
+
await file.writeFile(chunk);
|
|
157
|
+
}
|
|
158
|
+
await file.sync();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
throw new Error('Installer download interrupted or invalid; no installer was saved');
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
await file.close();
|
|
165
|
+
}
|
|
166
|
+
if (!size || hash.digest('hex') !== release.sha256)
|
|
167
|
+
throw new Error('Installer checksum verification failed; no installer was saved');
|
|
168
|
+
// Atomic publication without overwriting a file or following a destination symlink.
|
|
169
|
+
await link(partial, output);
|
|
170
|
+
return output;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await rm(temporary, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function defaultInstallerPath(release, environment) {
|
|
177
|
+
return join(homedir(), 'Downloads', `Zalos-${environment}-${release.version}-mac-arm64.dmg`);
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=desktop-update.js.map
|
|
@@ -54,7 +54,13 @@ export function runDatabaseTool(args) {
|
|
|
54
54
|
'database_identity',
|
|
55
55
|
'database_secret',
|
|
56
56
|
'tunnel_start',
|
|
57
|
+
'tunnel_exit',
|
|
57
58
|
'tunnel_readiness',
|
|
59
|
+
'ssm_plugin_missing',
|
|
60
|
+
'ssm_access_denied',
|
|
61
|
+
'ssm_target_not_connected',
|
|
62
|
+
'ssm_data_channel',
|
|
63
|
+
'tunnel_port_in_use',
|
|
58
64
|
'ca_bundle',
|
|
59
65
|
];
|
|
60
66
|
if (typeof payload.code === 'string' && known.includes(payload.code))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zalosai/zalos-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.18",
|
|
4
4
|
"description": "API-only product command-line interface for Zalos",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -42,13 +42,14 @@
|
|
|
42
42
|
"@tiptap/pm": "3.27.1",
|
|
43
43
|
"@tiptap/static-renderer": "3.27.1",
|
|
44
44
|
"@tiptap/starter-kit": "3.27.1",
|
|
45
|
-
"commander": "^13.1.0"
|
|
45
|
+
"commander": "^13.1.0",
|
|
46
|
+
"yaml": "2.9.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@types/node": "^20.0.0",
|
|
49
50
|
"tsx": "^4.0.0",
|
|
50
51
|
"typescript": "^5.0.0",
|
|
51
|
-
"vitest": "
|
|
52
|
+
"vitest": "3.2.7"
|
|
52
53
|
},
|
|
53
54
|
"publishConfig": {
|
|
54
55
|
"access": "public",
|
|
@@ -17,9 +17,10 @@ If the request is to reproduce a third-party portal, preserve that portal's appe
|
|
|
17
17
|
## Packaging and navigation
|
|
18
18
|
|
|
19
19
|
Produce one HTML file with embedded CSS, fonts, JavaScript, and data/SVG images, under the 1,000,000 UTF-8 byte upload limit. No CDN scripts, network fetches, external images, cookies, localStorage, popup windows, or dependence on the parent DOM. The iframe has scripts enabled but no same-origin privileges. Use in-memory state. Prefer type=button and click handlers; remote form submission is blocked.
|
|
20
|
-
Use `mesh.navigate('/agents')` in an HTML button to navigate within the current Mesh team. Preview shows the destination without navigating. Workflow launch actions are unavailable in Custom Pages.
|
|
20
|
+
Use `mesh.navigate('/agents')` in an HTML button to navigate within the current Mesh team. Existing links with a current-team `/t/<team-id>/...` path also navigate in the host. Preview shows the destination without navigating. Workflow launch actions are unavailable in Custom Pages.
|
|
21
21
|
Preserve the behavior of supplied examples when importing them; replace unsupported actions only when requested.
|
|
22
22
|
For a button that opens another page in the current Mesh team, call `mesh.navigate('/agents')` with an app-local path. Preview reports the destination without navigating. Full URLs, protocol-relative URLs, and another team's `/t/...` path are rejected. The host applies the current team prefix; do not build it into the HTML.
|
|
23
|
+
For a self-contained export, call `mesh.download('report.csv', csv, 'text/csv;charset=utf-8')`. It downloads generated UTF-8 content up to 10 MB. To export a binary file, pass base64 content as the second argument and `base64` as the fourth argument. The host never fetches download content from a URL.
|
|
23
24
|
|
|
24
25
|
## Iteration
|
|
25
26
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: update-zalos
|
|
3
|
+
description: Check for and download an approved Zalos Desktop update when the user asks to update the desktop app. Installation of the current macOS preview is manual.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Update Zalos Desktop
|
|
7
|
+
|
|
8
|
+
Use the bundled `zl` command. It updates the desktop application, not the CLI independently.
|
|
9
|
+
Keep the existing desktop login and environment; do not use global developer credentials, switch teams, or choose another region to bypass a denied request.
|
|
10
|
+
|
|
11
|
+
1. Run `zl desktop update --check`. The desktop wrapper supplies the environment and authorization. Outside the wrapper, select the known environment explicitly with `--environment staging|us-prod`; do not guess it.
|
|
12
|
+
2. Read the JSON result. For `unpublished`, `up-to-date`, or `installed-newer`, explain that result and stop. If `compatible` is false, explain the minimum macOS version. Never downgrade or replace staging with production.
|
|
13
|
+
3. When the user has asked to update and a compatible newer release is available, run `zl desktop update`. The command streams the approved installer and verifies its SHA256 checksum. An existing destination is never overwritten; choose another `--output` path if needed.
|
|
14
|
+
4. A `downloaded` result means the installer is ready, not that Zalos has been updated. Show the local installer path and the returned `nextSteps` before the user quits the app: finish running tasks, quit Zalos, open the DMG, replace the matching app, and reopen it. This chat stops when Zalos quits. Help open the verified local DMG if requested, but leave replacement to the user.
|
|
15
|
+
5. After reopening, run the check again to confirm the installed version.
|
|
16
|
+
|
|
17
|
+
If app detection fails, locate the user's installed Zalos application and pass its path with `--app`. Do not substitute the CLI version for the desktop version.
|
|
18
|
+
On 401/403, guide the user to reconnect their Zalos account; on unavailable release metadata, stop and report it. Never fetch installers from arbitrary web search results, print tokens or signed download links, remove quarantine attributes, or modify a running app. Use macOS's normal Open Anyway interaction for the unsigned preview when required.
|
package/skills/zl-ops/SKILL.md
CHANGED
|
@@ -114,3 +114,26 @@ zl schema <resource>
|
|
|
114
114
|
Successful product commands emit JSON on stdout. Errors emit structured JSON on stderr
|
|
115
115
|
and exit nonzero. Keep operational output focused on the requested object and report
|
|
116
116
|
what was inspected, previewed, applied, and published.
|
|
117
|
+
|
|
118
|
+
## Desktop product analytics
|
|
119
|
+
|
|
120
|
+
For “study chat from desktop apps last 24 hours”, read content-free reporting packets:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
zl --environment <us-prod|eu-prod> desktop metrics --since 24h
|
|
124
|
+
zl --environment <us-prod|eu-prod> desktop metrics --since 7d --user <user-id>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Only deployment-allowlisted human team owners with read scope can inspect the selected
|
|
128
|
+
team. Never switch teams or credentials to evade a denial. Use the returned `since`,
|
|
129
|
+
`until`, and `snapshot` for every `--cursor` page. Follow `nextCursor` to completion or
|
|
130
|
+
state the coverage limit. Sum matching metric names and dimension sets across packets;
|
|
131
|
+
never sum different metric families as if they were distinct requests. Packet periods
|
|
132
|
+
are whole buckets overlapping the selected window and can include activity outside it;
|
|
133
|
+
they are not exact event timestamps. Counters
|
|
134
|
+
are not conversations or unique-user counts. Latency/retry values are buckets, not
|
|
135
|
+
exact measurements. Unknown or omitted dimensions and missing packets are unavailable
|
|
136
|
+
coverage, not success. Identify recurring error classes, slow buckets, cancellations,
|
|
137
|
+
and token use; cite package IDs and reporting periods. Do not infer reply quality from
|
|
138
|
+
these metrics. Messages, transcripts, tool arguments/results and raw errors are absent.
|
|
139
|
+
Conversation sharing is disabled. Metrics and read audits expire after 90 days.
|