@shardflux/cli 0.24.0 → 0.25.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +148 -0
  2. package/README.md +263 -1
  3. package/dist/account-args.js +5 -0
  4. package/dist/account-pool.js +5 -1
  5. package/dist/acp-journal.js +23 -1
  6. package/dist/acp-permission-elsewhere.d.ts +38 -0
  7. package/dist/acp-permission-elsewhere.js +85 -0
  8. package/dist/acp-rpc.d.ts +2 -1
  9. package/dist/acp-rpc.js +11 -2
  10. package/dist/acp.js +95 -22
  11. package/dist/agent-credentials.d.ts +3 -0
  12. package/dist/agent-credentials.js +7 -0
  13. package/dist/args.d.ts +1 -1
  14. package/dist/args.js +172 -9
  15. package/dist/board-args.d.ts +6 -0
  16. package/dist/board-args.js +172 -0
  17. package/dist/board-commands.d.ts +62 -0
  18. package/dist/board-commands.js +412 -0
  19. package/dist/codex-cloud.d.ts +30 -3
  20. package/dist/codex-cloud.js +86 -12
  21. package/dist/conversation-commands.d.ts +30 -0
  22. package/dist/conversation-commands.js +328 -9
  23. package/dist/conversation-transcript.d.ts +44 -0
  24. package/dist/conversation-transcript.js +206 -0
  25. package/dist/conversations.d.ts +7 -3
  26. package/dist/conversations.js +20 -10
  27. package/dist/device-approvals.js +4 -3
  28. package/dist/device-bridge.d.ts +42 -1
  29. package/dist/device-bridge.js +223 -12
  30. package/dist/device-folders.d.ts +66 -0
  31. package/dist/device-folders.js +236 -0
  32. package/dist/device-helper.d.ts +6 -0
  33. package/dist/device-helper.js +98 -14
  34. package/dist/drive-commands.d.ts +25 -0
  35. package/dist/drive-commands.js +201 -0
  36. package/dist/errors.js +2 -1
  37. package/dist/format.js +6 -2
  38. package/dist/handlers.js +7 -1
  39. package/dist/land.d.ts +116 -0
  40. package/dist/land.js +238 -0
  41. package/dist/main.js +10 -1
  42. package/dist/move-files.d.ts +39 -0
  43. package/dist/move-files.js +167 -0
  44. package/dist/move.js +12 -11
  45. package/dist/onboarding.d.ts +10 -6
  46. package/dist/onboarding.js +24 -6
  47. package/dist/project-git.d.ts +7 -1
  48. package/dist/project-git.js +16 -3
  49. package/dist/project-preparation.d.ts +7 -0
  50. package/dist/project-preparation.js +14 -0
  51. package/dist/project-template-setup.js +32 -11
  52. package/dist/router-commands.d.ts +33 -0
  53. package/dist/router-commands.js +179 -0
  54. package/dist/task-land-hub.py +150 -0
  55. package/dist/transcribe-commands.d.ts +7 -0
  56. package/dist/transcribe-commands.js +80 -0
  57. package/dist/worktree-twin-engine.d.ts +8 -0
  58. package/dist/worktree-twin-engine.js +28 -22
  59. package/package.json +2 -2
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `shard transcribe <file>` (0.25.0+): what a recording says, with each word's time, through @shardflux/sdk's
3
+ * `transcriptions.transcribe()` (GET /v1/transcriptions). The signed-in session or SHARDFLUX_API_KEY, like
4
+ * `shard route` and `shard ask`; the audio goes to the API only and is not kept.
5
+ */
6
+ import { readFile } from 'node:fs/promises';
7
+ import { extname } from 'node:path';
8
+ import { audioFormatOf } from '@shardflux/sdk';
9
+ import { out, str } from "./commands.js";
10
+ import { EXIT, UsageError } from "./errors.js";
11
+ import { inScope } from "./router-commands.js";
12
+ /** What one transcription holds (the API's bound; past it the rest is not transcribed and `truncated` says so). */
13
+ export const TRANSCRIBE_MAX_BYTES = 6 * 1024 * 1024;
14
+ const FORMATS = ['webm', 'wav', 'pcm16'];
15
+ const BY_EXTENSION = { '.webm': 'webm', '.weba': 'webm', '.wav': 'wav', '.wave': 'wav', '.pcm': 'pcm16', '.raw': 'pcm16' };
16
+ const SAMPLE_RATES = [8000, 16000, 22050, 24000, 32000, 44100, 48000];
17
+ /** `12.34` seconds as `0:12.34`. */
18
+ const clock = (s) => `${Math.floor(s / 60)}:${(s % 60).toFixed(2).padStart(5, '0')}`;
19
+ /** The readable form: the text, then (with --words) one line per word with its start and end. */
20
+ export function transcriptText(t, words) {
21
+ const lines = [t.text || '(no speech)'];
22
+ if (t.truncated)
23
+ lines.push(`(only the first ${Math.round(t.duration)} s were transcribed)`);
24
+ if (words)
25
+ for (const w of t.words)
26
+ lines.push(`${clock(w.start)} ${clock(w.end)} ${w.w}`);
27
+ return `${lines.join('\n')}\n`;
28
+ }
29
+ export const TRANSCRIBE_HANDLERS = {
30
+ async transcribe(ctx, values, [file]) {
31
+ if (!file)
32
+ throw new UsageError('missing <file> for "shard transcribe"');
33
+ const named = str(values.format);
34
+ if (named !== undefined && !FORMATS.includes(named))
35
+ throw new UsageError(`--format must be one of ${FORMATS.join(', ')}`);
36
+ const rateText = str(values['sample-rate']);
37
+ const sampleRate = rateText === undefined ? undefined : Number(rateText);
38
+ if (sampleRate !== undefined && !SAMPLE_RATES.includes(sampleRate))
39
+ throw new UsageError(`--sample-rate must be one of ${SAMPLE_RATES.join(', ')}`);
40
+ const language = str(values.language);
41
+ if (language !== undefined && !/^[a-z]{2,3}$/.test(language))
42
+ throw new UsageError('--language must be an ISO 639-1 code such as en or fi');
43
+ const bytes = await readFile(file).catch((e) => {
44
+ throw new UsageError(`cannot read "${file}" (${e.code === 'EISDIR' ? 'a folder' : e.code ?? e.message})`);
45
+ });
46
+ const format = named ?? audioFormatOf(bytes) ?? BY_EXTENSION[extname(file).toLowerCase()];
47
+ if (!format)
48
+ throw new UsageError(`cannot tell the audio format of "${file}"`, 'pass --format webm|wav|pcm16 (WebM/Opus, PCM WAV or raw 16-bit mono)');
49
+ if (format === 'pcm16' && sampleRate === undefined)
50
+ throw new UsageError('raw pcm16 audio needs --sample-rate (for example 16000)');
51
+ if (format !== 'pcm16' && sampleRate !== undefined)
52
+ throw new UsageError('--sample-rate is only for --format pcm16');
53
+ if (bytes.length > TRANSCRIBE_MAX_BYTES)
54
+ ctx.io.stderr.write(`Only the first ${TRANSCRIBE_MAX_BYTES / 1024 / 1024} MB (and at most 120 s) of "${file}" is transcribed.\n`);
55
+ const audio = bytes.subarray(0, TRANSCRIBE_MAX_BYTES);
56
+ try {
57
+ const transcript = await inScope(ctx, values, (client, scope) => client.transcriptions.transcribe(audio, {
58
+ format,
59
+ ...(sampleRate === undefined ? {} : { sampleRate }),
60
+ ...(language === undefined ? {} : { language }),
61
+ ...scope,
62
+ signal: ctx.signal,
63
+ onEvent: (e) => {
64
+ if (e.type === 'queued')
65
+ ctx.io.stderr.write(`Queued: position ${e.position}, about ${Math.max(1, Math.round(e.expectedWaitMs / 1000))} s\n`);
66
+ },
67
+ }));
68
+ if (ctx.json)
69
+ out(ctx, '', transcript);
70
+ else
71
+ ctx.io.stdout.write(transcriptText(transcript, values.words === true));
72
+ return EXIT.ok;
73
+ }
74
+ catch (err) {
75
+ if (ctx.signal.aborted)
76
+ return EXIT.interrupted;
77
+ throw err;
78
+ }
79
+ },
80
+ };
@@ -119,6 +119,14 @@ export interface LoopDeps {
119
119
  * runs immediately. The first pass that succeeds after failures clears them too and runs again at once.
120
120
  */
121
121
  export declare function runTwinLoop(deps: LoopDeps, signal: AbortSignal): Promise<void>;
122
+ /**
123
+ * A VM of the project that reads the project hub: an awake one reads it without waking anything; else the project's
124
+ * small space reader (woken). The twin lands from it when a conversation's VM is gone; `shard task land` merges there.
125
+ */
126
+ export declare function projectHubCell(ctx: Ctx, projectId: string, signal?: AbortSignal, agentLabel?: string): Promise<{
127
+ cell: TwinCell;
128
+ close(): void;
129
+ }>;
122
130
  /** The device helper's twin. Nothing here asks the user anything. */
123
131
  export declare function runWorktreeTwin(ctx: Ctx, device: string, signal: AbortSignal): Promise<void>;
124
132
  export declare const TWIN_HANDLERS: Record<string, Handler>;
@@ -500,13 +500,36 @@ function reachable(url) {
500
500
  });
501
501
  }
502
502
  const gone = (error) => error.status === 404 || error.details?.reason === 'workspace_deleted';
503
+ const openCell = (ws, agentLabel) => {
504
+ const cell = ws.cell({ agentLabel });
505
+ return { cell, close: () => cell.close() };
506
+ };
507
+ /**
508
+ * A VM of the project that reads the project hub: an awake one reads it without waking anything; else the project's
509
+ * small space reader (woken). The twin lands from it when a conversation's VM is gone; `shard task land` merges there.
510
+ */
511
+ export async function projectHubCell(ctx, projectId, signal, agentLabel = 'shard-twin') {
512
+ const client = conversationClient(ctx);
513
+ const all = (await listConversations(ctx, projectId)).filter(c => !ended(c))
514
+ .sort((a, b) => (b.last_activity_at ?? '').localeCompare(a.last_activity_at ?? ''));
515
+ for (const c of all) {
516
+ try {
517
+ const ws = await client.workspaces.get(c.workspace_id);
518
+ if (!ws.data.deleted_at && ws.ready)
519
+ return openCell(ws, agentLabel);
520
+ }
521
+ catch { /* another may be awake */ }
522
+ }
523
+ const reader = await client.conversations.spaceReader(projectId, { ...(signal ? { signal } : {}) });
524
+ const ws = await client.workspaces.get(reader.workspace.id);
525
+ if (!ws.ready)
526
+ await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, ...(signal ? { signal } : {}) });
527
+ return openCell(ws, agentLabel);
528
+ }
503
529
  /** The device helper's twin. Nothing here asks the user anything. */
504
530
  export async function runWorktreeTwin(ctx, device, signal) {
505
531
  const client = () => conversationClient(ctx);
506
- const open = (ws) => {
507
- const cell = ws.cell({ agentLabel: 'shard-twin' });
508
- return { cell, close: () => cell.close() };
509
- };
532
+ const open = (ws) => openCell(ws, 'shard-twin');
510
533
  const engine = new TwinEngine({
511
534
  records: () => Object.values(localConversations(ctx)),
512
535
  conversations: async (projectId) => await listConversations(ctx, projectId),
@@ -539,24 +562,7 @@ export async function runWorktreeTwin(ctx, device, signal) {
539
562
  await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal });
540
563
  return open(ws);
541
564
  },
542
- hubCell: async (projectId) => {
543
- // An awake VM of the project reads the hub without waking anything; else the project's small space reader.
544
- const all = (await listConversations(ctx, projectId)).filter(c => !ended(c))
545
- .sort((a, b) => (b.last_activity_at ?? '').localeCompare(a.last_activity_at ?? ''));
546
- for (const c of all) {
547
- try {
548
- const ws = await client().workspaces.get(c.workspace_id);
549
- if (!ws.data.deleted_at && ws.ready)
550
- return open(ws);
551
- }
552
- catch { /* another may be awake */ }
553
- }
554
- const reader = await client().conversations.spaceReader(projectId, { signal });
555
- const ws = await client().workspaces.get(reader.workspace.id);
556
- if (!ws.ready)
557
- await ws.wake({ timeoutMs: ctx.wakeTimeoutMs, signal });
558
- return open(ws);
559
- },
565
+ hubCell: projectId => projectHubCell(ctx, projectId, signal),
560
566
  resume: async (c) => { await client().conversations.resume(c.id, { signal }); },
561
567
  land: async (c, action, into) => { await client().conversations.land(c.id, action, { into, signal }); },
562
568
  message: async (c, text) => { await client().conversations.send(c.id, text, { signal }); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork serverless agent workspaces.",
6
6
  "license": "Apache-2.0",
@@ -48,7 +48,7 @@
48
48
  "snappyjs": "0.7.0",
49
49
  "ws": "8.22.0",
50
50
  "yaml": "2.9.1",
51
- "@shardflux/sdk": "^0.27.2"
51
+ "@shardflux/sdk": "^0.28.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@eslint/js": "10.0.1",