@agentuity/cli 1.0.30 → 1.0.32

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 (119) hide show
  1. package/dist/agent-detection.js +1 -1
  2. package/dist/agent-detection.js.map +1 -1
  3. package/dist/api.d.ts +1 -1
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +1 -1
  6. package/dist/api.js.map +1 -1
  7. package/dist/cache/agent-intro.d.ts.map +1 -1
  8. package/dist/cache/agent-intro.js.map +1 -1
  9. package/dist/cache/resource-region.d.ts.map +1 -1
  10. package/dist/cache/resource-region.js.map +1 -1
  11. package/dist/cache/user-cache.d.ts.map +1 -1
  12. package/dist/cache/user-cache.js.map +1 -1
  13. package/dist/cmd/ai/opencode/install.js +1 -1
  14. package/dist/cmd/ai/opencode/install.js.map +1 -1
  15. package/dist/cmd/build/ast.js +2 -2
  16. package/dist/cmd/build/ast.js.map +1 -1
  17. package/dist/cmd/build/patch/_util.d.ts +1 -1
  18. package/dist/cmd/build/patch/_util.d.ts.map +1 -1
  19. package/dist/cmd/build/patch/_util.js +2 -2
  20. package/dist/cmd/build/patch/_util.js.map +1 -1
  21. package/dist/cmd/build/vite/bun-dev-server.js +9 -1
  22. package/dist/cmd/build/vite/bun-dev-server.js.map +1 -1
  23. package/dist/cmd/build/vite/config-loader.js +9 -1
  24. package/dist/cmd/build/vite/config-loader.js.map +1 -1
  25. package/dist/cmd/build/vite/registry-generator.js.map +1 -1
  26. package/dist/cmd/build/vite/server-bundler.js +9 -1
  27. package/dist/cmd/build/vite/server-bundler.js.map +1 -1
  28. package/dist/cmd/build/vite/static-renderer.js +11 -3
  29. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  30. package/dist/cmd/build/vite/vite-asset-server-config.js +9 -1
  31. package/dist/cmd/build/vite/vite-asset-server-config.js.map +1 -1
  32. package/dist/cmd/build/vite/vite-asset-server.js +9 -1
  33. package/dist/cmd/build/vite/vite-asset-server.js.map +1 -1
  34. package/dist/cmd/build/vite/vite-builder.js +10 -2
  35. package/dist/cmd/build/vite/vite-builder.js.map +1 -1
  36. package/dist/cmd/build/vite/workbench-generator.d.ts +1 -1
  37. package/dist/cmd/build/vite/workbench-generator.d.ts.map +1 -1
  38. package/dist/cmd/build/vite/workbench-generator.js +1 -1
  39. package/dist/cmd/build/vite/workbench-generator.js.map +1 -1
  40. package/dist/cmd/cloud/keyvalue/repl.d.ts.map +1 -1
  41. package/dist/cmd/cloud/keyvalue/repl.js +4 -2
  42. package/dist/cmd/cloud/keyvalue/repl.js.map +1 -1
  43. package/dist/cmd/cloud/keyvalue/search.d.ts.map +1 -1
  44. package/dist/cmd/cloud/keyvalue/search.js.map +1 -1
  45. package/dist/cmd/cloud/sandbox/cp.js +2 -2
  46. package/dist/cmd/cloud/sandbox/cp.js.map +1 -1
  47. package/dist/cmd/cloud/storage/config.d.ts.map +1 -1
  48. package/dist/cmd/cloud/storage/config.js +1 -2
  49. package/dist/cmd/cloud/storage/config.js.map +1 -1
  50. package/dist/cmd/cloud/storage/list.d.ts.map +1 -1
  51. package/dist/cmd/cloud/storage/list.js.map +1 -1
  52. package/dist/cmd/cloud/task/create.d.ts.map +1 -1
  53. package/dist/cmd/cloud/task/create.js +15 -6
  54. package/dist/cmd/cloud/task/create.js.map +1 -1
  55. package/dist/cmd/cloud/task/delete.d.ts +8 -0
  56. package/dist/cmd/cloud/task/delete.d.ts.map +1 -0
  57. package/dist/cmd/cloud/task/delete.js +286 -0
  58. package/dist/cmd/cloud/task/delete.js.map +1 -0
  59. package/dist/cmd/cloud/task/get.d.ts.map +1 -1
  60. package/dist/cmd/cloud/task/get.js +10 -3
  61. package/dist/cmd/cloud/task/get.js.map +1 -1
  62. package/dist/cmd/cloud/task/index.d.ts.map +1 -1
  63. package/dist/cmd/cloud/task/index.js +10 -0
  64. package/dist/cmd/cloud/task/index.js.map +1 -1
  65. package/dist/cmd/cloud/task/list.d.ts.map +1 -1
  66. package/dist/cmd/cloud/task/list.js +2 -0
  67. package/dist/cmd/cloud/task/list.js.map +1 -1
  68. package/dist/cmd/dev/dev-lock.js +2 -2
  69. package/dist/cmd/dev/dev-lock.js.map +1 -1
  70. package/dist/cmd/dev/templates.d.ts.map +1 -1
  71. package/dist/cmd/dev/templates.js +0 -1
  72. package/dist/cmd/dev/templates.js.map +1 -1
  73. package/dist/cmd/project/download.js +1 -1
  74. package/dist/cmd/project/download.js.map +1 -1
  75. package/dist/domain.js +1 -1
  76. package/dist/domain.js.map +1 -1
  77. package/dist/schema-generator.d.ts +1 -1
  78. package/dist/schema-generator.d.ts.map +1 -1
  79. package/dist/schema-generator.js +1 -1
  80. package/dist/schema-generator.js.map +1 -1
  81. package/dist/schema-parser.js +1 -1
  82. package/dist/schema-parser.js.map +1 -1
  83. package/dist/terminal.d.ts.map +1 -1
  84. package/dist/terminal.js +12 -17
  85. package/dist/terminal.js.map +1 -1
  86. package/dist/tui.js +1 -1
  87. package/dist/tui.js.map +1 -1
  88. package/dist/utils/date.js +1 -1
  89. package/dist/utils/date.js.map +1 -1
  90. package/package.json +6 -6
  91. package/src/agent-detection.ts +1 -1
  92. package/src/api.ts +1 -1
  93. package/src/cache/agent-intro.ts +3 -4
  94. package/src/cache/resource-region.ts +3 -1
  95. package/src/cache/user-cache.ts +3 -4
  96. package/src/cmd/ai/opencode/install.ts +1 -1
  97. package/src/cmd/build/ast.ts +2 -2
  98. package/src/cmd/build/patch/_util.ts +2 -2
  99. package/src/cmd/build/vite/registry-generator.ts +4 -4
  100. package/src/cmd/build/vite/workbench-generator.ts +1 -1
  101. package/src/cmd/cloud/keyvalue/repl.ts +6 -2
  102. package/src/cmd/cloud/keyvalue/search.ts +2 -1
  103. package/src/cmd/cloud/sandbox/cp.ts +2 -2
  104. package/src/cmd/cloud/storage/config.ts +3 -8
  105. package/src/cmd/cloud/storage/list.ts +8 -3
  106. package/src/cmd/cloud/task/create.ts +17 -8
  107. package/src/cmd/cloud/task/delete.ts +342 -0
  108. package/src/cmd/cloud/task/get.ts +11 -3
  109. package/src/cmd/cloud/task/index.ts +10 -0
  110. package/src/cmd/cloud/task/list.ts +2 -0
  111. package/src/cmd/dev/dev-lock.ts +2 -2
  112. package/src/cmd/dev/templates.ts +0 -1
  113. package/src/cmd/project/download.ts +1 -1
  114. package/src/domain.ts +3 -3
  115. package/src/schema-generator.ts +1 -1
  116. package/src/schema-parser.ts +1 -1
  117. package/src/terminal.ts +12 -14
  118. package/src/tui.ts +1 -1
  119. package/src/utils/date.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/cli",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Agentuity employees and contributors",
6
6
  "type": "module",
@@ -41,9 +41,9 @@
41
41
  "prepublishOnly": "bun run clean && bun run build"
42
42
  },
43
43
  "dependencies": {
44
- "@agentuity/auth": "1.0.30",
45
- "@agentuity/core": "1.0.30",
46
- "@agentuity/server": "1.0.30",
44
+ "@agentuity/auth": "1.0.32",
45
+ "@agentuity/core": "1.0.32",
46
+ "@agentuity/server": "1.0.32",
47
47
  "@datasert/cronjs-parser": "^1.4.0",
48
48
  "@vitejs/plugin-react": "^5.1.2",
49
49
  "acorn-loose": "^8.5.2",
@@ -60,10 +60,10 @@
60
60
  "typescript": "^5.9.0",
61
61
  "vite": "^7.2.7",
62
62
  "zod": "^4.3.5",
63
- "@agentuity/frontend": "1.0.30"
63
+ "@agentuity/frontend": "1.0.32"
64
64
  },
65
65
  "devDependencies": {
66
- "@agentuity/test-utils": "1.0.30",
66
+ "@agentuity/test-utils": "1.0.32",
67
67
  "@types/adm-zip": "^0.5.7",
68
68
  "@types/bun": "latest",
69
69
  "@types/tar-fs": "^2.0.4",
@@ -294,7 +294,7 @@ function initLinuxFFI(): FFIFunctions {
294
294
  const ppidField = fields[1]; // ppid is 2nd field after state
295
295
  if (!ppidField) return null;
296
296
  const ppid = parseInt(ppidField, 10);
297
- return isNaN(ppid) || ppid <= 1 ? null : ppid;
297
+ return Number.isNaN(ppid) || ppid <= 1 ? null : ppid;
298
298
  } catch {
299
299
  // Ignore errors
300
300
  }
package/src/api.ts CHANGED
@@ -93,7 +93,7 @@ export function getAppBaseURL(config?: Config | null): string {
93
93
  return baseGetAppBaseURL(config?.name, overrides);
94
94
  }
95
95
 
96
- export function getGravityDevModeURL(region: string, config?: Config | null): string {
96
+ export function getGravityDevModeURL(_region: string, config?: Config | null): string {
97
97
  const overrides = config?.overrides as { gravity_url?: string } | undefined;
98
98
  if (overrides?.gravity_url) {
99
99
  return overrides.gravity_url;
@@ -33,10 +33,9 @@ function getDatabase(): Database {
33
33
  export function hasAgentSeenIntro(agentId: string): boolean {
34
34
  try {
35
35
  const row = getDatabase()
36
- .query<
37
- { agent_id: string },
38
- [string]
39
- >('SELECT agent_id FROM agent_intro_seen WHERE agent_id = ?')
36
+ .query<{ agent_id: string }, [string]>(
37
+ 'SELECT agent_id FROM agent_intro_seen WHERE agent_id = ?'
38
+ )
40
39
  .get(agentId);
41
40
  return row !== null;
42
41
  } catch {
@@ -92,7 +92,9 @@ export async function getResourceInfo(
92
92
  .query<
93
93
  { region: string; org_id: string | null; project_id: string | null; last_updated: number },
94
94
  [string, string, string]
95
- >('SELECT region, org_id, project_id, last_updated FROM resource_region_cache WHERE resource_type = ? AND profile = ? AND id = ?')
95
+ >(
96
+ 'SELECT region, org_id, project_id, last_updated FROM resource_region_cache WHERE resource_type = ? AND profile = ? AND id = ?'
97
+ )
96
98
  .get(type, profile, id);
97
99
 
98
100
  if (!row) {
@@ -37,10 +37,9 @@ export function getCachedUserInfo(
37
37
  ): { userId: string; firstName: string; lastName: string } | null {
38
38
  try {
39
39
  const row = getDatabase()
40
- .query<
41
- { user_id: string; first_name: string; last_name: string },
42
- [string]
43
- >('SELECT user_id, first_name, last_name FROM user_info_cache WHERE profile = ?')
40
+ .query<{ user_id: string; first_name: string; last_name: string }, [string]>(
41
+ 'SELECT user_id, first_name, last_name FROM user_info_cache WHERE profile = ?'
42
+ )
44
43
  .get(profile);
45
44
  if (!row) return null;
46
45
  return {
@@ -56,7 +56,7 @@ export const installSubcommand = createSubcommand({
56
56
  const hasExactEntry = openCodeConfig.plugin.includes(pluginEntry);
57
57
 
58
58
  // Check if there's an existing entry that needs updating
59
- const existingIndex = openCodeConfig.plugin.findIndex((p) => p === '@agentuity/opencode');
59
+ const existingIndex = openCodeConfig.plugin.indexOf('@agentuity/opencode');
60
60
 
61
61
  if (hasExactEntry) {
62
62
  if (!jsonMode) {
@@ -1269,9 +1269,9 @@ function extractValidatorSchemas(callExpr: ASTCallExpression): {
1269
1269
  if (unary.argument?.type === 'Identifier') {
1270
1270
  const identifier = unary.argument as ASTNodeIdentifier;
1271
1271
  if (identifier.name === 'true') {
1272
- result.stream = unary.operator === '!' ? false : true;
1272
+ result.stream = unary.operator !== '!';
1273
1273
  } else if (identifier.name === 'false') {
1274
- result.stream = unary.operator === '!' ? true : false;
1274
+ result.stream = unary.operator === '!';
1275
1275
  }
1276
1276
  }
1277
1277
  }
@@ -43,7 +43,7 @@ ${inject}
43
43
 
44
44
  export function generateGatewayEnvGuard(
45
45
  apikey: string,
46
- apikeyval: string,
46
+ _apikeyval: string,
47
47
  apibase: string,
48
48
  provider: string
49
49
  ): string {
@@ -63,7 +63,7 @@ export function generateGatewayEnvGuard(
63
63
 
64
64
  export function searchBackwards(contents: string, offset: number, val: string): number {
65
65
  for (let i = offset; i >= 0; i--) {
66
- if (contents.charAt(i) == val) {
66
+ if (contents.charAt(i) === val) {
67
67
  return i;
68
68
  }
69
69
  }
@@ -1067,11 +1067,11 @@ export async function generateRouteRegistry(
1067
1067
  const generatedContent = `// @generated
1068
1068
  // Auto-generated by Agentuity - DO NOT EDIT
1069
1069
  ${importsStr}${typeImports}${
1070
- shouldEmitFrontendClient
1071
- ? `
1070
+ shouldEmitFrontendClient
1071
+ ? `
1072
1072
  import { createClient } from '@agentuity/frontend';`
1073
- : ''
1074
- }
1073
+ : ''
1074
+ }
1075
1075
  // ============================================================================
1076
1076
  // Route Schema Type Exports
1077
1077
  // ============================================================================
@@ -99,7 +99,7 @@ function generateIndexHtml(): string {
99
99
  */
100
100
  export async function generateWorkbenchFiles(
101
101
  rootDir: string,
102
- projectId: string,
102
+ _projectId: string,
103
103
  config: WorkbenchConfig,
104
104
  logger: Logger
105
105
  ): Promise<void> {
@@ -52,7 +52,8 @@ export const replSubcommand = createCommand({
52
52
  const contentType = isPossiblyJSON(ctx.parsed.args[2]!)
53
53
  ? 'application/json'
54
54
  : 'text/plain';
55
- const ttl = ctx.parsed.args.length > 3 ? parseInt(ctx.parsed.args[3]!) : undefined;
55
+ const ttl =
56
+ ctx.parsed.args.length > 3 ? parseInt(ctx.parsed.args[3]!, 10) : undefined;
56
57
  await storage.set(ctx.parsed.args[0]!, ctx.parsed.args[1]!, ctx.parsed.args[2]!, {
57
58
  contentType,
58
59
  ttl,
@@ -172,7 +173,10 @@ export const replSubcommand = createCommand({
172
173
  const item = results[key];
173
174
  if (!item) continue;
174
175
  const sizeMB = (item.size / (1024 * 1024)).toFixed(2);
175
- const date = item.lastUsed != null ? new Date(item.lastUsed).toLocaleString() : 'unknown';
176
+ const date =
177
+ item.lastUsed != null
178
+ ? new Date(item.lastUsed).toLocaleString()
179
+ : 'unknown';
176
180
  ctx.write(
177
181
  ` ${tui.bold(key)}: ${sizeMB} MB, ${item.contentType}, updated ${date}`
178
182
  );
@@ -58,7 +58,8 @@ export const searchSubcommand = createCommand({
58
58
  const item = results[key];
59
59
  if (!item) continue;
60
60
  const sizeMB = (item.size / (1024 * 1024)).toFixed(2);
61
- const date = item.lastUsed != null ? new Date(item.lastUsed).toLocaleString() : 'unknown';
61
+ const date =
62
+ item.lastUsed != null ? new Date(item.lastUsed).toLocaleString() : 'unknown';
62
63
  tui.info(` ${tui.bold(key)}: ${sizeMB} MB, ${item.contentType}, updated ${date}`);
63
64
  }
64
65
  }
@@ -209,7 +209,7 @@ async function uploadToSandbox(
209
209
 
210
210
  async function uploadSingleFile(
211
211
  client: APIClient,
212
- logger: Logger,
212
+ _logger: Logger,
213
213
  orgId: string,
214
214
  sandboxId: string,
215
215
  resolvedPath: string,
@@ -327,7 +327,7 @@ async function downloadFromSandbox(
327
327
 
328
328
  async function downloadSingleFile(
329
329
  client: APIClient,
330
- logger: Logger,
330
+ _logger: Logger,
331
331
  orgId: string,
332
332
  sandboxId: string,
333
333
  remotePath: string,
@@ -18,20 +18,15 @@ import { getResourceInfo, setResourceInfo } from '../../../cache';
18
18
  function displayConfig(config: BucketConfig) {
19
19
  tui.newline();
20
20
  console.log(tui.bold('Bucket: ') + config.bucket_name);
21
+ console.log(tui.bold('Storage Tier: ') + (config.storage_tier ?? tui.muted('default')));
21
22
  console.log(
22
- tui.bold('Storage Tier: ') + (config.storage_tier ?? tui.muted('default'))
23
- );
24
- console.log(
25
- tui.bold('TTL: ') +
26
- (config.ttl != null ? `${config.ttl}s` : tui.muted('default'))
23
+ tui.bold('TTL: ') + (config.ttl != null ? `${config.ttl}s` : tui.muted('default'))
27
24
  );
28
25
  console.log(
29
26
  tui.bold('Public: ') +
30
27
  (config.public != null ? String(config.public) : tui.muted('default'))
31
28
  );
32
- console.log(
33
- tui.bold('Cache Control: ') + (config.cache_control ?? tui.muted('default'))
34
- );
29
+ console.log(tui.bold('Cache Control: ') + (config.cache_control ?? tui.muted('default')));
35
30
 
36
31
  if (config.cors) {
37
32
  console.log(tui.bold('CORS:'));
@@ -260,15 +260,20 @@ export const listSubcommand = createSubcommand({
260
260
  if (s3.region) console.log(` Region: ${tui.muted(s3.region)}`);
261
261
  if (s3.endpoint) console.log(` Endpoint: ${tui.muted(s3.endpoint)}`);
262
262
  if (s3.object_count != null) {
263
- const sizeStr = s3.total_size != null ? tui.formatBytes(s3.total_size) : 'unknown';
264
- console.log(` Objects: ${tui.muted(`${s3.object_count.toLocaleString()} (${sizeStr})`)}`);
263
+ const sizeStr =
264
+ s3.total_size != null ? tui.formatBytes(s3.total_size) : 'unknown';
265
+ console.log(
266
+ ` Objects: ${tui.muted(`${s3.object_count.toLocaleString()} (${sizeStr})`)}`
267
+ );
265
268
  }
266
269
  if (s3.last_event_at) {
267
270
  const date = new Date(s3.last_event_at);
268
271
  if (Number.isNaN(date.getTime())) {
269
272
  console.log(` Activity: ${tui.muted('unknown')}`);
270
273
  } else {
271
- console.log(` Activity: ${tui.muted(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }))}`);
274
+ console.log(
275
+ ` Activity: ${tui.muted(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }))}`
276
+ );
272
277
  }
273
278
  }
274
279
  tui.newline();
@@ -5,7 +5,7 @@ import * as tui from '../../../tui';
5
5
  import { createStorageAdapter, parseMetadataFlag, cacheTaskId } from './util';
6
6
  import { getCommand } from '../../../command-prefix';
7
7
  import { whoami } from '@agentuity/server';
8
- import type { TaskPriority, TaskStatus, TaskType } from '@agentuity/core';
8
+ import type { TaskPriority, TaskStatus, TaskType, UserType } from '@agentuity/core';
9
9
  import { getCachedUserInfo, setCachedUserInfo } from '../../../cache';
10
10
  import { defaultProfileName } from '../../../config';
11
11
 
@@ -70,6 +70,10 @@ export const createSubcommand = createCommand({
70
70
  .min(1)
71
71
  .optional()
72
72
  .describe('the display name of the creator (used with --created-id)'),
73
+ createdType: z
74
+ .enum(['human', 'agent'])
75
+ .optional()
76
+ .describe('the type of the creator - human user or AI agent (default: human)'),
73
77
  projectId: z.string().optional().describe('project ID to associate with the task'),
74
78
  projectName: z
75
79
  .string()
@@ -101,18 +105,23 @@ export const createSubcommand = createCommand({
101
105
 
102
106
  // Resolve creator info
103
107
  const createdId = opts.createdId ?? ctx.auth.userId;
104
- let creator: { id: string; name: string } | undefined;
105
- if (opts.createdId && opts.createdName) {
106
- // Explicit creator with name
107
- creator = { id: opts.createdId, name: opts.createdName };
108
- } else if (!opts.createdId) {
108
+ const createdType = (opts.createdType as UserType) ?? 'human';
109
+ let creator: { id: string; name: string; type?: UserType } | undefined;
110
+ if (opts.createdId) {
111
+ // Explicit creator use createdId as name fallback (like project pattern)
112
+ creator = {
113
+ id: opts.createdId,
114
+ name: opts.createdName ?? opts.createdId,
115
+ type: createdType,
116
+ };
117
+ } else {
109
118
  // Using auth userId — check cache first, then fall back to whoami API call
110
119
  const profileName = ctx.config?.name ?? defaultProfileName;
111
120
  const cached = getCachedUserInfo(profileName);
112
121
  if (cached) {
113
122
  const name = [cached.firstName, cached.lastName].filter(Boolean).join(' ');
114
123
  if (name) {
115
- creator = { id: createdId, name };
124
+ creator = { id: createdId, name, type: createdType };
116
125
  }
117
126
  } else {
118
127
  // Fetch from API and cache
@@ -120,7 +129,7 @@ export const createSubcommand = createCommand({
120
129
  const user = await whoami(ctx.apiClient);
121
130
  const name = [user.firstName, user.lastName].filter(Boolean).join(' ');
122
131
  if (name) {
123
- creator = { id: createdId, name };
132
+ creator = { id: createdId, name, type: createdType };
124
133
  }
125
134
  setCachedUserInfo(profileName, createdId, user.firstName, user.lastName);
126
135
  } catch {
@@ -0,0 +1,342 @@
1
+ import { z } from 'zod';
2
+ import { createCommand } from '../../../types';
3
+ import * as tui from '../../../tui';
4
+ import { createStorageAdapter } from './util';
5
+ import { getCommand } from '../../../command-prefix';
6
+ import { isDryRunMode, outputDryRun } from '../../../explain';
7
+ import type { TaskPriority, TaskStatus, TaskType, BatchDeletedTask } from '@agentuity/core';
8
+
9
+ const DURATION_UNITS: Record<string, number> = {
10
+ s: 1000,
11
+ m: 60 * 1000,
12
+ h: 60 * 60 * 1000,
13
+ d: 24 * 60 * 60 * 1000,
14
+ w: 7 * 24 * 60 * 60 * 1000,
15
+ };
16
+
17
+ /**
18
+ * Parse a human-friendly duration string (e.g. "30s", "7d", "24h", "30m", "2w")
19
+ * into milliseconds. Exported for testing.
20
+ */
21
+ export function parseDuration(duration: string): number {
22
+ const match = duration.match(/^(\d+)([smhdw])$/);
23
+ if (!match) {
24
+ tui.fatal(
25
+ `Invalid duration format: "${duration}". Use a number followed by s (seconds), m (minutes), h (hours), d (days), or w (weeks). Examples: 30s, 30m, 24h, 7d, 2w`
26
+ );
27
+ // tui.fatal exits, but TypeScript doesn't know that
28
+ throw new Error('unreachable');
29
+ }
30
+ const value = parseInt(match[1]!, 10);
31
+ const unit = match[2]!;
32
+ const ms = DURATION_UNITS[unit];
33
+ if (!ms) {
34
+ tui.fatal(`Unknown duration unit: "${unit}"`);
35
+ throw new Error('unreachable');
36
+ }
37
+ return value * ms;
38
+ }
39
+
40
+ function truncate(s: string, max: number): string {
41
+ if (s.length <= max) return s;
42
+ return `${s.slice(0, max - 1)}…`;
43
+ }
44
+
45
+ const TaskDeleteResponseSchema = z.object({
46
+ success: z.boolean().describe('Whether the operation succeeded'),
47
+ deleted: z
48
+ .array(
49
+ z.object({
50
+ id: z.string().describe('Deleted task ID'),
51
+ title: z.string().describe('Deleted task title'),
52
+ })
53
+ )
54
+ .describe('List of deleted tasks'),
55
+ count: z.number().describe('Number of tasks deleted'),
56
+ durationMs: z.number().describe('Operation duration in milliseconds'),
57
+ dryRun: z.boolean().optional().describe('Whether this was a dry run'),
58
+ message: z.string().optional().describe('Status message'),
59
+ });
60
+
61
+ export const deleteSubcommand = createCommand({
62
+ name: 'delete',
63
+ aliases: ['del', 'rm'],
64
+ description: 'Soft-delete a task by ID or batch-delete tasks by filter',
65
+ tags: ['destructive', 'deletes-resource', 'slow', 'requires-auth'],
66
+ requires: { auth: true },
67
+ examples: [
68
+ {
69
+ command: getCommand('cloud task delete task_abc123'),
70
+ description: 'Delete a single task by ID',
71
+ },
72
+ {
73
+ command: getCommand('cloud task delete --status closed --older-than 7d'),
74
+ description: 'Delete closed tasks older than 7 days',
75
+ },
76
+ {
77
+ command: getCommand('cloud task delete --status done --limit 10 --dry-run'),
78
+ description: 'Preview which done tasks would be deleted (dry run)',
79
+ },
80
+ {
81
+ command: getCommand('cloud task delete --status cancelled --confirm'),
82
+ description: 'Delete all cancelled tasks without confirmation prompt',
83
+ },
84
+ ],
85
+ schema: {
86
+ args: z.object({
87
+ id: z.string().optional().describe('Task ID to delete (for single delete)'),
88
+ }),
89
+ options: z.object({
90
+ status: z
91
+ .enum(['open', 'in_progress', 'done', 'closed', 'cancelled'])
92
+ .optional()
93
+ .describe('filter batch delete by status'),
94
+ type: z
95
+ .enum(['epic', 'feature', 'enhancement', 'bug', 'task'])
96
+ .optional()
97
+ .describe('filter batch delete by type'),
98
+ priority: z
99
+ .enum(['high', 'medium', 'low', 'none'])
100
+ .optional()
101
+ .describe('filter batch delete by priority'),
102
+ olderThan: z
103
+ .string()
104
+ .optional()
105
+ .describe('filter batch delete by age (e.g. 30s, 7d, 24h, 2w)'),
106
+ parentId: z.string().optional().describe('filter batch delete by parent task ID'),
107
+ createdId: z.string().optional().describe('filter batch delete by creator ID'),
108
+ limit: z.coerce
109
+ .number()
110
+ .int()
111
+ .min(1)
112
+ .max(200)
113
+ .default(50)
114
+ .describe('max tasks to delete in batch mode (default: 50, max: 200)'),
115
+ confirm: z.boolean().optional().default(false).describe('skip confirmation prompt'),
116
+ }),
117
+ response: TaskDeleteResponseSchema,
118
+ },
119
+
120
+ async handler(ctx) {
121
+ const { args, opts, options } = ctx;
122
+ const started = Date.now();
123
+ const storage = await createStorageAdapter(ctx);
124
+
125
+ // Determine mode: single delete or batch delete
126
+ const isSingleDelete = !!args.id;
127
+ const hasFilters =
128
+ opts.status ||
129
+ opts.type ||
130
+ opts.priority ||
131
+ opts.olderThan ||
132
+ opts.parentId ||
133
+ opts.createdId;
134
+
135
+ if (!isSingleDelete && !hasFilters) {
136
+ tui.fatal(
137
+ 'Provide a task ID for single delete, or use --status, --type, --priority, --older-than, --parent-id, or --created-id for batch delete.'
138
+ );
139
+ }
140
+
141
+ if (isSingleDelete && hasFilters) {
142
+ tui.fatal(
143
+ 'Cannot combine task ID with filter options. Use either single delete (by ID) or batch delete (by filters).'
144
+ );
145
+ }
146
+
147
+ // ── Single delete mode ──────────────────────────────────────────────
148
+ if (isSingleDelete) {
149
+ if (isDryRunMode(options)) {
150
+ outputDryRun(`Would soft-delete task: ${args.id}`, options);
151
+ return {
152
+ success: true,
153
+ deleted: [{ id: args.id!, title: '(dry run)' }],
154
+ count: 1,
155
+ durationMs: Date.now() - started,
156
+ dryRun: true,
157
+ message: 'Dry run — no tasks were deleted',
158
+ };
159
+ }
160
+
161
+ if (!opts.confirm) {
162
+ const confirmed = await tui.confirm(`Delete task "${args.id}"?`, false);
163
+ if (!confirmed) {
164
+ if (!options.json) tui.info('Cancelled');
165
+ return {
166
+ success: false,
167
+ deleted: [],
168
+ count: 0,
169
+ durationMs: Date.now() - started,
170
+ message: 'Cancelled',
171
+ };
172
+ }
173
+ }
174
+
175
+ const task = await storage.softDelete(args.id!);
176
+ const durationMs = Date.now() - started;
177
+
178
+ if (!options.json) {
179
+ tui.success(`Deleted task ${tui.bold(task.id)} (${task.title}) in ${durationMs}ms`);
180
+ }
181
+
182
+ return {
183
+ success: true,
184
+ deleted: [{ id: task.id, title: task.title }],
185
+ count: 1,
186
+ durationMs,
187
+ };
188
+ }
189
+
190
+ // ── Batch delete mode ───────────────────────────────────────────────
191
+ // Validate older-than format early (before calling the API)
192
+ if (opts.olderThan) {
193
+ parseDuration(opts.olderThan); // will fatal on invalid format
194
+ }
195
+
196
+ const batchParams = {
197
+ status: opts.status as TaskStatus | undefined,
198
+ type: opts.type as TaskType | undefined,
199
+ priority: opts.priority as TaskPriority | undefined,
200
+ parent_id: opts.parentId,
201
+ created_id: opts.createdId,
202
+ older_than: opts.olderThan,
203
+ limit: opts.limit,
204
+ };
205
+
206
+ // For dry-run and preview, first list what would be matched
207
+ // (we call batchDelete only when actually executing)
208
+ if (isDryRunMode(options) || !opts.confirm) {
209
+ // Use list() to preview matching tasks
210
+ const preview = await storage.list({
211
+ status: batchParams.status,
212
+ type: batchParams.type,
213
+ priority: batchParams.priority,
214
+ parent_id: batchParams.parent_id,
215
+ limit: batchParams.limit,
216
+ sort: 'created_at',
217
+ order: 'asc',
218
+ });
219
+
220
+ // Client-side filters for preview (server will apply these on actual delete)
221
+ let candidates = preview.tasks;
222
+ if (batchParams.created_id) {
223
+ candidates = candidates.filter(
224
+ (t: { created_id: string }) => t.created_id === batchParams.created_id
225
+ );
226
+ }
227
+ if (opts.olderThan) {
228
+ const durationMs = parseDuration(opts.olderThan);
229
+ const cutoff = new Date(Date.now() - durationMs);
230
+ candidates = candidates.filter(
231
+ (t: { created_at: string }) => new Date(t.created_at) < cutoff
232
+ );
233
+ }
234
+
235
+ if (candidates.length === 0) {
236
+ if (!options.json) tui.info('No tasks match the given filters');
237
+ return {
238
+ success: true,
239
+ deleted: [],
240
+ count: 0,
241
+ durationMs: Date.now() - started,
242
+ message: 'No matching tasks found',
243
+ };
244
+ }
245
+
246
+ // Show preview table
247
+ if (!options.json) {
248
+ tui.warning(
249
+ `Found ${candidates.length} ${tui.plural(candidates.length, 'task', 'tasks')} to delete:`
250
+ );
251
+ tui.newline();
252
+
253
+ const tableData = candidates.map(
254
+ (task: {
255
+ id: string;
256
+ title: string;
257
+ status: string;
258
+ type: string;
259
+ created_at: string;
260
+ }) => ({
261
+ ID: tui.muted(truncate(task.id, 28)),
262
+ Title: truncate(task.title, 40),
263
+ Status: task.status,
264
+ Type: task.type,
265
+ Created: new Date(task.created_at).toLocaleDateString(),
266
+ })
267
+ );
268
+
269
+ tui.table(tableData, [
270
+ { name: 'ID', alignment: 'left' },
271
+ { name: 'Title', alignment: 'left' },
272
+ { name: 'Status', alignment: 'left' },
273
+ { name: 'Type', alignment: 'left' },
274
+ { name: 'Created', alignment: 'left' },
275
+ ]);
276
+ tui.newline();
277
+ }
278
+
279
+ // Dry-run: return preview without executing
280
+ if (isDryRunMode(options)) {
281
+ outputDryRun(
282
+ `Would soft-delete ${candidates.length} ${tui.plural(candidates.length, 'task', 'tasks')}`,
283
+ options
284
+ );
285
+ return {
286
+ success: true,
287
+ deleted: candidates.map(
288
+ (t: { id: string; title: string }): BatchDeletedTask => ({
289
+ id: t.id,
290
+ title: t.title,
291
+ })
292
+ ),
293
+ count: candidates.length,
294
+ durationMs: Date.now() - started,
295
+ dryRun: true,
296
+ message: 'Dry run — no tasks were deleted',
297
+ };
298
+ }
299
+
300
+ // Confirmation prompt
301
+ if (!opts.confirm) {
302
+ const confirmed = await tui.confirm(
303
+ `Delete ${candidates.length} ${tui.plural(candidates.length, 'task', 'tasks')}?`,
304
+ false
305
+ );
306
+ if (!confirmed) {
307
+ if (!options.json) tui.info('Cancelled');
308
+ return {
309
+ success: false,
310
+ deleted: [],
311
+ count: 0,
312
+ durationMs: Date.now() - started,
313
+ message: 'Cancelled',
314
+ };
315
+ }
316
+ }
317
+ }
318
+
319
+ // Execute batch delete via server-side API
320
+ const result = await storage.batchDelete(batchParams);
321
+ const durationMs = Date.now() - started;
322
+
323
+ if (!options.json) {
324
+ if (result.count > 0) {
325
+ tui.success(
326
+ `Deleted ${result.count} ${tui.plural(result.count, 'task', 'tasks')} in ${durationMs}ms`
327
+ );
328
+ } else {
329
+ tui.info('No tasks matched the given filters');
330
+ }
331
+ }
332
+
333
+ return {
334
+ success: true,
335
+ deleted: result.deleted,
336
+ count: result.count,
337
+ durationMs,
338
+ };
339
+ },
340
+ });
341
+
342
+ export default deleteSubcommand;