@4ge/cli 0.1.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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/bin/dev.js +3 -0
  4. package/bin/run.js +3 -0
  5. package/dist/base.js +398 -0
  6. package/dist/commands/auth/login.js +197 -0
  7. package/dist/commands/auth/logout.js +31 -0
  8. package/dist/commands/auth/profile/delete.js +34 -0
  9. package/dist/commands/auth/profile/list.js +32 -0
  10. package/dist/commands/auth/profile/use.js +30 -0
  11. package/dist/commands/auth/status.js +107 -0
  12. package/dist/commands/config/get.js +37 -0
  13. package/dist/commands/config/index.js +9 -0
  14. package/dist/commands/config/manage.js +165 -0
  15. package/dist/commands/config/set.js +42 -0
  16. package/dist/commands/config/show.js +46 -0
  17. package/dist/commands/epic/index.js +10 -0
  18. package/dist/commands/epic/list.js +91 -0
  19. package/dist/commands/epic/pull.js +137 -0
  20. package/dist/commands/epic/show.js +67 -0
  21. package/dist/commands/epic/update.js +104 -0
  22. package/dist/commands/feature/index.js +2 -0
  23. package/dist/commands/feature/list.js +93 -0
  24. package/dist/commands/feature/pull.js +122 -0
  25. package/dist/commands/feature/show.js +60 -0
  26. package/dist/commands/feature/update.js +103 -0
  27. package/dist/commands/idea/create.js +88 -0
  28. package/dist/commands/idea/graduate.js +89 -0
  29. package/dist/commands/idea/index.js +5 -0
  30. package/dist/commands/idea/list.js +83 -0
  31. package/dist/commands/idea/show.js +86 -0
  32. package/dist/commands/idea/update.js +108 -0
  33. package/dist/commands/init.js +354 -0
  34. package/dist/commands/login.js +5 -0
  35. package/dist/commands/plan/index.js +10 -0
  36. package/dist/commands/plan/pull.js +104 -0
  37. package/dist/commands/plan/versions.js +64 -0
  38. package/dist/commands/project/create.js +84 -0
  39. package/dist/commands/project/index.js +9 -0
  40. package/dist/commands/project/info.js +74 -0
  41. package/dist/commands/project/list.js +78 -0
  42. package/dist/commands/project/pull.js +127 -0
  43. package/dist/commands/project/update.js +104 -0
  44. package/dist/commands/root.js +13 -0
  45. package/dist/commands/skill/index.js +8 -0
  46. package/dist/commands/skill/install.js +71 -0
  47. package/dist/commands/story/index.js +10 -0
  48. package/dist/commands/story/list.js +81 -0
  49. package/dist/commands/story/pull.js +125 -0
  50. package/dist/commands/story/show.js +57 -0
  51. package/dist/commands/story/update.js +103 -0
  52. package/dist/commands/template/create.js +72 -0
  53. package/dist/commands/template/delete.js +62 -0
  54. package/dist/commands/template/feature/create.js +70 -0
  55. package/dist/commands/template/feature/delete.js +75 -0
  56. package/dist/commands/template/feature/list.js +69 -0
  57. package/dist/commands/template/feature/update.js +82 -0
  58. package/dist/commands/template/info.js +52 -0
  59. package/dist/commands/template/list.js +76 -0
  60. package/dist/commands/template/pull.js +99 -0
  61. package/dist/commands/template/update.js +71 -0
  62. package/dist/commands/whoami.js +5 -0
  63. package/dist/core/api/action-hints.js +17 -0
  64. package/dist/core/api/index.js +3 -0
  65. package/dist/core/api/retry.js +95 -0
  66. package/dist/core/api/validate-response.js +41 -0
  67. package/dist/core/auth/api-client.js +156 -0
  68. package/dist/core/auth/config.js +3 -0
  69. package/dist/core/auth/credentials.js +143 -0
  70. package/dist/core/auth/email-auth.js +30 -0
  71. package/dist/core/auth/errors.js +20 -0
  72. package/dist/core/auth/index.js +6 -0
  73. package/dist/core/auth/realtime.js +82 -0
  74. package/dist/core/auth/resolution.js +63 -0
  75. package/dist/core/auth/state.js +9 -0
  76. package/dist/core/auth/token-refresh.js +100 -0
  77. package/dist/core/config/defaults.js +31 -0
  78. package/dist/core/config/index.js +3 -0
  79. package/dist/core/config/parser.js +213 -0
  80. package/dist/core/config/schema.js +29 -0
  81. package/dist/core/project/markdown.js +37 -0
  82. package/dist/core/skill/installer.js +59 -0
  83. package/dist/core/skill/paths.js +52 -0
  84. package/dist/generated/api/client/client.gen.js +217 -0
  85. package/dist/generated/api/client/index.js +6 -0
  86. package/dist/generated/api/client/types.gen.js +2 -0
  87. package/dist/generated/api/client/utils.gen.js +231 -0
  88. package/dist/generated/api/client.gen.js +3 -0
  89. package/dist/generated/api/core/auth.gen.js +14 -0
  90. package/dist/generated/api/core/bodySerializer.gen.js +57 -0
  91. package/dist/generated/api/core/params.gen.js +103 -0
  92. package/dist/generated/api/core/pathSerializer.gen.js +114 -0
  93. package/dist/generated/api/core/queryKeySerializer.gen.js +99 -0
  94. package/dist/generated/api/core/serverSentEvents.gen.js +136 -0
  95. package/dist/generated/api/core/types.gen.js +2 -0
  96. package/dist/generated/api/core/utils.gen.js +87 -0
  97. package/dist/generated/api/index.js +2 -0
  98. package/dist/generated/api/sdk.gen.js +556 -0
  99. package/dist/generated/api/types.gen.js +2 -0
  100. package/dist/generated/api/zod.gen.js +1262 -0
  101. package/dist/ui/components/action-hints.js +33 -0
  102. package/dist/ui/components/cli-header.js +91 -0
  103. package/dist/ui/components/confirm-prompt.js +28 -0
  104. package/dist/ui/components/data-table.js +35 -0
  105. package/dist/ui/components/detail-view.js +5 -0
  106. package/dist/ui/components/error-display.js +5 -0
  107. package/dist/ui/components/prompt-layout.js +6 -0
  108. package/dist/ui/components/select-prompt.js +28 -0
  109. package/dist/ui/components/spinner.js +14 -0
  110. package/dist/ui/components/success-message.js +5 -0
  111. package/dist/ui/components/text-input.js +29 -0
  112. package/dist/ui/hooks/use-action-hints.js +21 -0
  113. package/dist/ui/hooks/use-status.js +22 -0
  114. package/dist/ui/index.js +15 -0
  115. package/dist/ui/prompt-helpers.js +55 -0
  116. package/dist/ui/render.js +9 -0
  117. package/package.json +89 -0
  118. package/skills/4ge-cli.md +113 -0
  119. package/usage.md +320 -0
@@ -0,0 +1,82 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../../base.js';
4
+ import { updateTemplateFeature } from '../../../generated/api/sdk.gen.js';
5
+ import { zUpdateTemplateFeatureResponse } from '../../../generated/api/zod.gen.js';
6
+ import { validateResponse } from '../../../core/api/index.js';
7
+ import { SuccessMessage } from '../../../ui/index.js';
8
+ export default class TemplateFeatureUpdate extends BaseCommand {
9
+ static description = 'Update a feature in a template';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ id: Flags.string({
13
+ description: 'Template ID',
14
+ required: true,
15
+ }),
16
+ 'feature-id': Flags.string({
17
+ description: 'Feature ID',
18
+ required: true,
19
+ }),
20
+ name: Flags.string({
21
+ description: 'Feature name',
22
+ }),
23
+ description: Flags.string({
24
+ description: 'Feature description',
25
+ }),
26
+ public: Flags.boolean({
27
+ description: 'Whether the feature is public',
28
+ allowNo: true,
29
+ }),
30
+ };
31
+ static examples = [
32
+ '<%= config.bin %> template:feature:update --id <template_id> --feature-id <feature_id> --name "New Name"',
33
+ '<%= config.bin %> template:feature:update --id <template_id> --feature-id <feature_id> --public',
34
+ '<%= config.bin %> template:feature:update --id <template_id> --feature-id <feature_id> --no-public',
35
+ ];
36
+ async run() {
37
+ const { flags } = await this.parse(TemplateFeatureUpdate);
38
+ await this.renderHeader('ready', 'compact');
39
+ const payload = {
40
+ path: {
41
+ templateId: flags.id,
42
+ featureId: flags['feature-id'],
43
+ },
44
+ body: {
45
+ ...(flags.name !== undefined && { name: flags.name }),
46
+ ...(flags.description !== undefined && { description: flags.description }),
47
+ ...(flags.public !== undefined && { is_public: flags.public }),
48
+ },
49
+ };
50
+ if (this.isDryRun) {
51
+ await this.handleDryRun(payload);
52
+ return;
53
+ }
54
+ const client = await this.getClient();
55
+ const { data, error } = await this.withSpinner('Updating template feature...', () => updateTemplateFeature({
56
+ client,
57
+ ...payload,
58
+ }));
59
+ try {
60
+ const validated = validateResponse(data, error, zUpdateTemplateFeatureResponse);
61
+ const feature = validated.data;
62
+ const metadata = validated.metadata;
63
+ if (this.isJsonMode) {
64
+ this.outputJson(feature, {
65
+ source: 'template feature update',
66
+ templateId: flags.id,
67
+ featureId: flags['feature-id'],
68
+ ...metadata,
69
+ });
70
+ }
71
+ else {
72
+ await this.renderInk(_jsx(SuccessMessage, { message: `Feature "${feature.name}" (id: ${feature.id}) has been updated in template ${flags.id}.` }));
73
+ if (this.isInteractiveMode && metadata?.action_hints) {
74
+ await this.handleActionHints(metadata.action_hints);
75
+ }
76
+ }
77
+ }
78
+ catch (err) {
79
+ this.handleError(`Failed to update feature ${flags['feature-id']} for template: ${flags.id}`, err);
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,52 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { getTemplateDetail } from '../../generated/api/sdk.gen.js';
4
+ import { zGetTemplateDetailResponse } from '../../generated/api/zod.gen.js';
5
+ import { validateResponse } from '../../core/api/index.js';
6
+ export default class TemplateInfo extends BaseCommand {
7
+ static description = 'Get details for a specific project template';
8
+ static flags = {
9
+ ...BaseCommand.baseFlags,
10
+ id: Flags.string({
11
+ description: 'Template ID',
12
+ required: true,
13
+ }),
14
+ };
15
+ static examples = [
16
+ '<%= config.bin %> template:info --id <template_id>',
17
+ '<%= config.bin %> template:info --id <template_id> --json',
18
+ ];
19
+ async run() {
20
+ const { flags } = await this.parse(TemplateInfo);
21
+ await this.renderHeader('ready', 'compact');
22
+ const client = await this.getClient();
23
+ try {
24
+ const { data, error } = await this.withRetry(() => getTemplateDetail({
25
+ client,
26
+ path: { templateId: flags.id },
27
+ }));
28
+ const validated = validateResponse(data, error, zGetTemplateDetailResponse);
29
+ const template = validated.data;
30
+ const metadata = validated.metadata;
31
+ if (this.isJsonMode) {
32
+ this.outputJson(template, { source: 'template info', ...metadata });
33
+ }
34
+ else {
35
+ this.log(`\nTemplate: ${template.name}`);
36
+ this.log(`ID: ${template.id}`);
37
+ this.log(`Author: ${template.author}`);
38
+ this.log(`Framework: ${template.framework}`);
39
+ this.log(`Official: ${template.isOfficial ? 'Yes' : 'No'}`);
40
+ this.log(`Features: ${template.featuresCount}`);
41
+ this.log(`Created: ${new Date(template.createdAt).toLocaleString()}`);
42
+ this.log(`\nDescription:\n${template.description || 'No description available.'}\n`);
43
+ if (this.isInteractiveMode && metadata?.action_hints) {
44
+ await this.handleActionHints(metadata.action_hints);
45
+ }
46
+ }
47
+ }
48
+ catch (err) {
49
+ this.handleError(`Failed to fetch template detail for ID: ${flags.id}`, err);
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { listTemplates } from '../../generated/api/sdk.gen.js';
5
+ import { zListTemplatesResponse } from '../../generated/api/zod.gen.js';
6
+ import { validateResponse } from '../../core/api/index.js';
7
+ import { DataTable } from '../../ui/index.js';
8
+ export default class TemplateList extends BaseCommand {
9
+ static description = 'List all available project templates';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ workspace: Flags.string({
13
+ description: 'Filter templates by workspace ID',
14
+ }),
15
+ };
16
+ static examples = [
17
+ '<%= config.bin %> template:list',
18
+ '<%= config.bin %> template:list --workspace <workspace_id>',
19
+ '<%= config.bin %> template:list --json',
20
+ ];
21
+ async run() {
22
+ const { flags } = await this.parse(TemplateList);
23
+ await this.renderHeader('ready', 'compact');
24
+ const client = await this.getClient();
25
+ const { data, error } = await this.withRetry(() => listTemplates({ client }));
26
+ let templates;
27
+ let metadata;
28
+ try {
29
+ const validated = validateResponse(data, error, zListTemplatesResponse);
30
+ templates = validated.data;
31
+ metadata = validated.metadata;
32
+ }
33
+ catch (err) {
34
+ this.handleError('Failed to fetch templates', err);
35
+ return;
36
+ }
37
+ // Filter by workspace if flag provided (client-side)
38
+ const filteredTemplates = flags.workspace
39
+ ? templates?.filter((t) => t.workspace_id === flags.workspace) || []
40
+ : templates || [];
41
+ if (this.isJsonMode) {
42
+ this.outputJson(filteredTemplates, { source: 'template list', ...metadata });
43
+ }
44
+ else {
45
+ if (filteredTemplates.length === 0) {
46
+ this.log(flags.workspace ? `No templates found for workspace: ${flags.workspace}` : 'No templates found.');
47
+ return;
48
+ }
49
+ await this.renderInk(_jsx(DataTable, { data: filteredTemplates, columns: [
50
+ { key: 'name', header: 'Name', minWidth: 20 },
51
+ { key: 'id', header: 'ID' },
52
+ { key: 'framework', header: 'Framework' },
53
+ { key: 'author', header: 'Author' },
54
+ { key: 'featuresCount', header: 'Features' },
55
+ { key: 'isOfficial', header: 'Official', format: (v) => (v ? '✓' : '') },
56
+ ], title: "Available Templates", emptyMessage: flags.workspace ? `No templates found for workspace: ${flags.workspace}` : 'No templates found.' }));
57
+ if (this.isInteractiveMode) {
58
+ if (filteredTemplates.length > 0) {
59
+ const options = filteredTemplates.map((item) => ({
60
+ label: item.name || item.id,
61
+ value: item.id,
62
+ description: item.framework || '',
63
+ }));
64
+ const selectedId = await this.selectOption('Select a template for details:', options, { headerVariant: 'compact' });
65
+ if (selectedId) {
66
+ const { default: chalk } = await import('chalk');
67
+ this.log(`\n 🚀 Run: ${chalk.cyan(`4ge template info --id ${selectedId}`)}\n`);
68
+ }
69
+ }
70
+ if (metadata?.action_hints) {
71
+ await this.handleActionHints(metadata.action_hints);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,99 @@
1
+ import { BaseCommand } from '../../base.js';
2
+ import { readConfig, getAssetPath } from '../../core/config/index.js';
3
+ import { getTemplateDetail, getProjectDetail } from '../../generated/api/sdk.gen.js';
4
+ import { zGetTemplateDetailResponse, zGetProjectDetailResponse } from '../../generated/api/zod.gen.js';
5
+ import { validateResponse } from '../../core/api/index.js';
6
+ import { SuccessMessage } from '../../ui/components/success-message.js';
7
+ import { Flags } from '@oclif/core';
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import React from 'react';
11
+ export default class TemplatePull extends BaseCommand {
12
+ static description = 'Pull template data from the cloud and save it locally';
13
+ static flags = {
14
+ ...BaseCommand.baseFlags,
15
+ save: Flags.boolean({
16
+ description: 'Force saving to disk even in --json mode',
17
+ default: false,
18
+ }),
19
+ stdout: Flags.boolean({
20
+ description: 'Print to terminal without saving (human mode only)',
21
+ default: false,
22
+ }),
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(TemplatePull);
26
+ if (flags.json && flags.stdout) {
27
+ this.error('Cannot use --json and --stdout together');
28
+ }
29
+ if (flags.save && flags.stdout) {
30
+ this.error('Cannot use --stdout and --save together');
31
+ }
32
+ await this.renderHeader('ready', 'compact');
33
+ await this.migrateAssetsIfNeeded();
34
+ const client = await this.getClient();
35
+ const config = await readConfig();
36
+ const projectId = config?.project_id;
37
+ if (!projectId) {
38
+ this.handleError('Project not initialized. Run `4ge init` first.', { code: 'NOT_INITIALIZED' });
39
+ return;
40
+ }
41
+ // 1. Get Project Detail to find template_id
42
+ let templateId;
43
+ try {
44
+ const { data, error } = await this.withSpinner('Fetching project details', () => getProjectDetail({
45
+ client,
46
+ path: { projectId },
47
+ }));
48
+ const validated = validateResponse(data, error, zGetProjectDetailResponse);
49
+ templateId = validated.data.project_type_id ?? undefined;
50
+ }
51
+ catch (err) {
52
+ this.handleError('Failed to fetch project details', err);
53
+ return;
54
+ }
55
+ if (!templateId) {
56
+ this.handleError('No template associated with this project (project_type_id is null).', { code: 'NO_TEMPLATE' });
57
+ return;
58
+ }
59
+ // 2. Fetch Template Detail
60
+ let templateData;
61
+ try {
62
+ const { data, error } = await this.withSpinner('Fetching template details', () => getTemplateDetail({
63
+ client,
64
+ path: { templateId },
65
+ }));
66
+ const validated = validateResponse(data, error, zGetTemplateDetailResponse);
67
+ templateData = validated.data;
68
+ }
69
+ catch (err) {
70
+ this.handleError('Failed to fetch template details', err);
71
+ return;
72
+ }
73
+ const savePath = getAssetPath('template.json');
74
+ // 3. Output Logic
75
+ if (this.isJsonMode) {
76
+ this.outputJson(templateData);
77
+ if (flags.save) {
78
+ await this.saveToFile(savePath, templateData);
79
+ }
80
+ }
81
+ else if (flags.stdout) {
82
+ this.log(JSON.stringify(templateData, null, 2));
83
+ }
84
+ else {
85
+ await this.saveToFile(savePath, templateData);
86
+ await this.renderInk(React.createElement(SuccessMessage, { message: 'Template data saved to .4ge/assets/template.json' }));
87
+ }
88
+ if (this.isInteractiveMode) {
89
+ await this.handleActionHints([
90
+ { label: 'List template features', command: `4ge template feature list ${templateId}` },
91
+ { label: 'Project info', command: '4ge project info' },
92
+ ]);
93
+ }
94
+ }
95
+ async saveToFile(filePath, data) {
96
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
97
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
98
+ }
99
+ }
@@ -0,0 +1,71 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { updateTemplate } from '../../generated/api/sdk.gen.js';
5
+ import { zUpdateTemplateResponse } from '../../generated/api/zod.gen.js';
6
+ import { validateResponse } from '../../core/api/index.js';
7
+ import { SuccessMessage } from '../../ui/index.js';
8
+ export default class TemplateUpdate extends BaseCommand {
9
+ static description = 'Update an existing project template';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ id: Flags.string({
13
+ description: 'Template ID',
14
+ required: true,
15
+ }),
16
+ name: Flags.string({
17
+ description: 'Template name',
18
+ }),
19
+ description: Flags.string({
20
+ description: 'Template description',
21
+ }),
22
+ force: Flags.boolean({
23
+ description: 'Skip confirmation prompt',
24
+ default: false,
25
+ }),
26
+ };
27
+ static examples = [
28
+ '<%= config.bin %> template:update --id <template_id> --name "New Name"',
29
+ '<%= config.bin %> template:update --id <template_id> --description "New description"',
30
+ ];
31
+ async run() {
32
+ const { flags } = await this.parse(TemplateUpdate);
33
+ await this.renderHeader('ready', 'compact');
34
+ const confirmed = await this.confirmAction(`Update template ${flags.id}?`, flags.force, { headerVariant: 'compact' });
35
+ if (!confirmed)
36
+ return;
37
+ const payload = {
38
+ path: { templateId: flags.id },
39
+ body: {
40
+ ...(flags.name !== undefined && { name: flags.name }),
41
+ ...(flags.description !== undefined && { description: flags.description }),
42
+ },
43
+ };
44
+ if (this.isDryRun) {
45
+ await this.handleDryRun(payload);
46
+ return;
47
+ }
48
+ const client = await this.getClient();
49
+ const { data, error } = await this.withSpinner('Updating template...', () => updateTemplate({
50
+ client,
51
+ ...payload,
52
+ }));
53
+ try {
54
+ const validated = validateResponse(data, error, zUpdateTemplateResponse);
55
+ const template = validated.data;
56
+ const metadata = validated.metadata;
57
+ if (this.isJsonMode) {
58
+ this.outputJson(template, { source: 'template update', ...metadata });
59
+ }
60
+ else {
61
+ await this.renderInk(_jsx(SuccessMessage, { message: `Template "${template.name}" (id: ${template.id}) has been updated.` }));
62
+ if (this.isInteractiveMode && metadata?.action_hints) {
63
+ await this.handleActionHints(metadata.action_hints);
64
+ }
65
+ }
66
+ }
67
+ catch (err) {
68
+ this.handleError(`Failed to update template: ${flags.id}`, err);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,5 @@
1
+ import AuthStatus from './auth/status.js';
2
+ export default class Whoami extends AuthStatus {
3
+ static description = 'Alias for `4ge auth status`';
4
+ static hidden = false;
5
+ }
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export const ActionHintSchema = z.object({
3
+ description: z.string().describe('Human-readable description of the suggested action'),
4
+ command: z.string().describe('The 4ge CLI command to execute'),
5
+ });
6
+ export const ActionHintsSchema = z.array(ActionHintSchema);
7
+ /**
8
+ * Parse and validate action hints from API metadata.
9
+ * Returns empty array if hints are missing, null, or invalid.
10
+ * Never throws — action hints are best-effort, not critical.
11
+ */
12
+ export function parseActionHints(raw) {
13
+ if (!Array.isArray(raw))
14
+ return [];
15
+ const result = ActionHintsSchema.safeParse(raw);
16
+ return result.success ? result.data : [];
17
+ }
@@ -0,0 +1,3 @@
1
+ export { validateResponse, extractData, ApiValidationError } from './validate-response.js';
2
+ export { parseActionHints } from './action-hints.js';
3
+ export { withRetry, computeBackoff, isRetryableResult, retryReason } from './retry.js';
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Retry helpers for transient API failures.
3
+ *
4
+ * The SDK returns `{ data?, error?, request, response }` and resolves (rather
5
+ * than throwing) on HTTP errors when `throwOnError` is false (the CLI default).
6
+ * A network failure (fetch throws) surfaces as `{ error, response: undefined }`.
7
+ * So retryability is decided by inspecting the resolved result.
8
+ */
9
+ const DEFAULTS = {
10
+ baseDelayMs: 500,
11
+ multiplier: 2,
12
+ jitter: 0.25,
13
+ };
14
+ /**
15
+ * Compute the backoff delay (ms) for a given 1-based attempt number, with
16
+ * exponential growth and symmetric jitter.
17
+ *
18
+ * delay = base * (mult ^ (attempt-1)) * (1 ± jitter)
19
+ */
20
+ export function computeBackoff(attempt, opts = {}) {
21
+ const base = opts.baseDelayMs ?? DEFAULTS.baseDelayMs;
22
+ const mult = opts.multiplier ?? DEFAULTS.multiplier;
23
+ const jitter = opts.jitter ?? DEFAULTS.jitter;
24
+ const exp = base * Math.pow(mult, attempt - 1);
25
+ // Symmetric jitter in [-jitter, +jitter].
26
+ const factor = 1 + (Math.random() * 2 - 1) * jitter;
27
+ return Math.max(0, Math.round(exp * factor));
28
+ }
29
+ /**
30
+ * Decide whether a resolved SDK result is a transient (retryable) failure.
31
+ *
32
+ * Retryable:
33
+ * - `response` missing (network failure — fetch threw)
34
+ * - HTTP 429 (Too Many Requests)
35
+ * - HTTP 5xx (server errors)
36
+ * Not retryable:
37
+ * - 4xx (except 429) — client errors, won't change on retry
38
+ * - success (no error)
39
+ */
40
+ export function isRetryableResult(result) {
41
+ if (result === null || typeof result !== 'object')
42
+ return false;
43
+ const r = result;
44
+ // Success path — no error, no retry.
45
+ if (r.error === undefined || r.error === null)
46
+ return false;
47
+ const status = r.response?.status;
48
+ if (status === undefined)
49
+ return true; // network failure (no response)
50
+ if (status === 429)
51
+ return true;
52
+ if (status >= 500 && status < 600)
53
+ return true;
54
+ return false;
55
+ }
56
+ /**
57
+ * Reason label for a retryable result, for reporting in metadata.
58
+ */
59
+ export function retryReason(result) {
60
+ if (result === null || typeof result !== 'object')
61
+ return 'unknown';
62
+ const r = result;
63
+ const status = r.response?.status;
64
+ if (status === undefined)
65
+ return 'network_error';
66
+ if (status === 429)
67
+ return 'rate_limited';
68
+ return `http_${status}`;
69
+ }
70
+ /**
71
+ * Run `fn`, retrying on transient failures with exponential backoff + jitter.
72
+ *
73
+ * Returns the final result plus the list of retry attempts (empty if the first
74
+ * call succeeded). `fn` must resolve to an SDK-shaped result; thrown errors are
75
+ * NOT retried (they indicate non-transient failures or post-call validation).
76
+ */
77
+ export async function withRetry(fn, opts) {
78
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
79
+ const attempts = [];
80
+ let lastResult;
81
+ for (let attempt = 0;; attempt++) {
82
+ lastResult = await fn();
83
+ if (attempt >= opts.maxAttempts || !isRetryableResult(lastResult)) {
84
+ return { result: lastResult, attempts };
85
+ }
86
+ const delayMs = computeBackoff(attempt + 1, opts);
87
+ attempts.push({
88
+ attempt: attempt + 1,
89
+ status: lastResult?.response?.status,
90
+ reason: retryReason(lastResult),
91
+ delayMs,
92
+ });
93
+ await sleep(delayMs);
94
+ }
95
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Validates an SDK response against a Zod schema at the boundary.
3
+ *
4
+ * @param sdkData - The `data` field from the SDK response (the API envelope body)
5
+ * @param sdkError - The `error` field from the SDK response (transport/HTTP errors)
6
+ * @param schema - The Zod schema for the expected API envelope shape
7
+ * @returns The validated API envelope with proper types
8
+ * @throws Error if transport error occurs or schema validation fails
9
+ */
10
+ export function validateResponse(sdkData, sdkError, schema) {
11
+ // 1. Check for transport/HTTP-level errors
12
+ if (sdkError) {
13
+ throw new ApiValidationError('API request failed', sdkError);
14
+ }
15
+ // 2. Validate the response body against the Zod schema
16
+ const result = schema.safeParse(sdkData);
17
+ if (!result.success) {
18
+ throw new ApiValidationError('API response validation failed', result.error);
19
+ }
20
+ return result.data;
21
+ }
22
+ /**
23
+ * Extracts just the data payload from a validated API response.
24
+ * Convenience function for the common case where you only need the data field.
25
+ */
26
+ export function extractData(sdkData, sdkError, schema) {
27
+ const validated = validateResponse(sdkData, sdkError, schema);
28
+ return validated.data;
29
+ }
30
+ /**
31
+ * Custom error class for API validation failures.
32
+ * Provides structured error info for both human and agent consumption.
33
+ */
34
+ export class ApiValidationError extends Error {
35
+ details;
36
+ constructor(message, details) {
37
+ super(message);
38
+ this.name = 'ApiValidationError';
39
+ this.details = details;
40
+ }
41
+ }