@orchagent/cli 0.3.94 → 0.3.96

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.
@@ -217,7 +217,9 @@ function registerDagCommand(program) {
217
217
  spinner.stop();
218
218
  const msg = e instanceof Error ? e.message : 'Unknown error';
219
219
  if (msg.includes('404') || msg.includes('not found') || msg.includes('Not part of')) {
220
- throw new errors_1.CliError(`Run ${resolvedRunId.slice(0, 8)}... is not part of an orchestration chain.`);
220
+ throw new errors_1.CliError(`Run ${resolvedRunId.slice(0, 8)}... is not part of an orchestration chain.\n\n` +
221
+ `The DAG view is only available for runs that call other agents via the SDK.\n` +
222
+ `Verify that your agent uses the Orchestrator SDK to invoke other agents.`);
221
223
  }
222
224
  throw e;
223
225
  }
@@ -33,6 +33,24 @@ async function resolveWorkspace(config, workspaceSlug) {
33
33
  }
34
34
  return workspace;
35
35
  }
36
+ async function resolveCurrentWorkspace(config) {
37
+ const configFile = await (0, config_1.loadConfig)();
38
+ const response = await (0, api_1.request)(config, 'GET', '/workspaces');
39
+ // If user has a default workspace configured, use it
40
+ if (configFile.workspace) {
41
+ const workspace = response.workspaces.find((w) => w.slug === configFile.workspace);
42
+ if (workspace) {
43
+ return workspace;
44
+ }
45
+ }
46
+ // If only one workspace, use it
47
+ if (response.workspaces.length === 1) {
48
+ return response.workspaces[0];
49
+ }
50
+ // Multiple workspaces and no default — this shouldn't happen in fork flow,
51
+ // but if we get here, throw an error
52
+ throw new errors_1.CliError('Multiple workspaces available. Use `orch workspace use <slug>` to set default or `--workspace <slug>` to specify.');
53
+ }
36
54
  function registerForkCommand(program) {
37
55
  program
38
56
  .command('fork <agent>')
@@ -101,7 +119,23 @@ Examples:
101
119
  return;
102
120
  }
103
121
  const forked = result.agent;
104
- const targetOrgSlug = forked.org_slug ?? targetWorkspace?.slug ?? 'current-workspace';
122
+ // Resolve the target workspace slug:
123
+ // 1. If org_slug is in response, use it (gateway knows what org it created the agent in)
124
+ // 2. If explicit --workspace was provided, use its slug
125
+ // 3. Otherwise, resolve current workspace
126
+ let targetOrgSlug;
127
+ if (forked.org_slug) {
128
+ targetOrgSlug = forked.org_slug;
129
+ }
130
+ else if (targetWorkspace) {
131
+ targetOrgSlug = targetWorkspace.slug;
132
+ }
133
+ else {
134
+ write('Resolving current workspace...\n');
135
+ const currentWorkspace = await resolveCurrentWorkspace(config);
136
+ targetOrgSlug = currentWorkspace.slug;
137
+ targetWorkspace = currentWorkspace;
138
+ }
105
139
  write(`\n${chalk_1.default.green('\u2713')} Forked ${org}/${agent}@${version}\n`);
106
140
  write(` New agent: ${targetOrgSlug}/${forked.name}@${forked.version}\n`);
107
141
  if (targetWorkspace) {
@@ -31,7 +31,12 @@ async function resolveWorkspaceId(config, slug) {
31
31
  if (response.workspaces.length === 1) {
32
32
  return response.workspaces[0].id;
33
33
  }
34
- // Multiple workspaces — list them and ask the user to pick
34
+ // Multiple workspaces — try to default to personal workspace
35
+ const personalWorkspace = response.workspaces.find((w) => w.type === 'personal');
36
+ if (personalWorkspace) {
37
+ return personalWorkspace.id;
38
+ }
39
+ // Multiple workspaces and no personal workspace found — ask the user to pick
35
40
  const slugs = response.workspaces.map((w) => w.slug).join(', ');
36
41
  throw new errors_1.CliError(`Multiple workspaces available: ${slugs}\n` +
37
42
  'Specify one with --workspace <slug> or run `orch workspace use <slug>`.');
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.localCommandForEntrypoint = localCommandForEntrypoint;
40
+ exports.canonicalAgentType = canonicalAgentType;
40
41
  exports.inferFileField = inferFileField;
41
42
  exports.validateInputSchema = validateInputSchema;
42
43
  exports.tryParseJsonObject = tryParseJsonObject;
@@ -97,7 +98,21 @@ function parseAgentRef(value) {
97
98
  }
98
99
  function canonicalAgentType(typeValue) {
99
100
  const normalized = (typeValue || 'agent').toLowerCase();
100
- return normalized === 'skill' ? 'skill' : 'agent';
101
+ // Handle legacy type names: agentic agent, code → tool
102
+ if (normalized === 'agentic')
103
+ return 'agent';
104
+ if (normalized === 'code')
105
+ return 'tool';
106
+ // Return the canonical type, defaulting to 'agent' for unrecognized types
107
+ if (normalized === 'prompt')
108
+ return 'prompt';
109
+ if (normalized === 'tool')
110
+ return 'tool';
111
+ if (normalized === 'agent')
112
+ return 'agent';
113
+ if (normalized === 'skill')
114
+ return 'skill';
115
+ return 'agent';
101
116
  }
102
117
  function resolveExecutionEngine(agentData) {
103
118
  if (agentData.execution_engine === 'direct_llm' || agentData.execution_engine === 'managed_loop' || agentData.execution_engine === 'code_runtime') {
@@ -1900,7 +1915,8 @@ async function executeCloud(agentRef, file, options) {
1900
1915
  }
1901
1916
  }
1902
1917
  // --estimate: show cost estimate before running and ask for confirmation
1903
- if (options.estimate) {
1918
+ // --estimate-only: show cost estimate and exit without running
1919
+ if (options.estimate || options.estimateOnly) {
1904
1920
  try {
1905
1921
  const est = await (0, api_1.getAgentCostEstimate)(resolved, org, parsed.agent, parsed.version);
1906
1922
  const e = est.estimate;
@@ -1926,7 +1942,11 @@ async function executeCloud(agentRef, file, options) {
1926
1942
  }
1927
1943
  process.stderr.write('\n');
1928
1944
  }
1929
- // Ask for confirmation
1945
+ // If --estimate-only, exit after showing estimate
1946
+ if (options.estimateOnly) {
1947
+ return;
1948
+ }
1949
+ // Otherwise, ask for confirmation (--estimate only)
1930
1950
  const rl = await Promise.resolve().then(() => __importStar(require('readline')));
1931
1951
  const iface = rl.createInterface({ input: process.stdin, output: process.stderr });
1932
1952
  const answer = await new Promise(resolve => {
@@ -1939,7 +1959,10 @@ async function executeCloud(agentRef, file, options) {
1939
1959
  }
1940
1960
  }
1941
1961
  catch {
1942
- // Non-fatal: if estimate fails, proceed with the run
1962
+ // Non-fatal: if estimate fails, proceed with the run (or exit if --estimate-only)
1963
+ if (options.estimateOnly) {
1964
+ throw new errors_1.CliError('Could not fetch cost estimate.');
1965
+ }
1943
1966
  process.stderr.write(chalk_1.default.gray('Could not fetch cost estimate. Proceeding...\n\n'));
1944
1967
  }
1945
1968
  }
@@ -2864,6 +2887,7 @@ function registerRunCommand(program) {
2864
2887
  .option('--json', 'Output raw JSON')
2865
2888
  .option('--verbose', 'Show sandbox stdout/stderr and debug info (cloud only)')
2866
2889
  .option('--estimate', 'Show cost estimate before running and ask for confirmation')
2890
+ .option('--estimate-only', 'Show cost estimate and exit (for CI/CD automation)')
2867
2891
  .option('--provider <provider>', 'LLM provider (openai, anthropic, gemini, ollama)')
2868
2892
  .option('--model <model>', 'LLM model to use (overrides agent default)')
2869
2893
  .option('--key <key>', 'LLM API key (overrides env vars)')
@@ -628,13 +628,27 @@ class Analyst:
628
628
 
629
629
  async def generate_weekly_summary(self, store: ActivityStore) -> str:
630
630
  """Generate an intelligent weekly summary from the activity window."""
631
+ from datetime import datetime, timezone
632
+
631
633
  activity_data = store.serialise_for_llm()
632
634
  repos = ", ".join(store.repos)
635
+ now = datetime.now(timezone.utc)
636
+ current_date = now.strftime("%d %b %Y")
637
+
638
+ # Period range from activity window
639
+ period_start = store.window.period_start.strftime("%d %b %Y") if store.window and store.window.period_start else "unknown"
640
+ period_end = store.window.period_end.strftime("%d %b %Y") if store.window and store.window.period_end else current_date
633
641
 
634
642
  system_prompt = self._summary_prompt.replace(
635
643
  "{team_name}", self.team_name
636
644
  ).replace(
637
645
  "{repos}", repos
646
+ ).replace(
647
+ "{current_date}", current_date
648
+ ).replace(
649
+ "{period_start}", period_start
650
+ ).replace(
651
+ "{period_end}", period_end
638
652
  ).replace(
639
653
  "{activity_data}", activity_data
640
654
  )
@@ -733,6 +747,9 @@ anthropic>=0.40.0
733
747
  // ─── prompts/weekly_summary.md ───────────────────────────────────────────────
734
748
  exports.TEMPLATE_WEEKLY_SUMMARY_PROMPT = `You are a senior engineering manager analysing your team's GitHub activity for the past week. Your job is to write a concise, insightful weekly summary that a CTO or team lead would actually want to read on Monday morning.
735
749
 
750
+ **Current date: {current_date}**
751
+ **Reporting period: {period_start} to {period_end}**
752
+
736
753
  Rules:
737
754
  - INTERPRET, don't just list. "3 PRs merged" is useless. "The auth refactor shipped -- 3 PRs merged across 2 repos" is useful.
738
755
  - Highlight what SHIPPED (merged PRs, significant commits). This is the headline.
@@ -1061,25 +1061,32 @@ async function runOnce(agentDir, dataJson, verbose, config) {
1061
1061
  function registerTestCommand(program) {
1062
1062
  program
1063
1063
  .command('test [path]')
1064
- .description('Validate agent configuration and run test suite')
1064
+ .description('Validate configuration and run test suite (fixtures + unit tests)')
1065
1065
  .option('-v, --verbose', 'Show detailed test output')
1066
1066
  .option('-w, --watch', 'Watch for file changes and re-run tests')
1067
1067
  .option('-r, --run', 'Run the agent once with --data input (validate first)')
1068
1068
  .option('-d, --data <json>', 'JSON input for --run mode')
1069
+ .option('--validate-only', 'Run validation only (skip test suite)')
1069
1070
  .addHelpText('after', `
1070
1071
  Examples:
1071
1072
  orch test Validate + run tests in current directory
1072
1073
  orch test ./my-agent Validate + run tests in specified directory
1073
1074
  orch test --verbose Show detailed test output
1074
1075
  orch test --watch Watch mode — re-run on file changes
1076
+ orch test --validate-only Validation only (same as: orch validate)
1075
1077
  orch test --run --data '{"task": "hello"}' Validate, then run once
1076
1078
 
1077
- What it checks:
1079
+ What it does (default):
1078
1080
  1. Validates orchagent.json (type, engine, required files, secrets, etc.)
1079
1081
  2. Runs Python tests (pytest): test_*.py, *_test.py
1080
1082
  3. Runs JS/TS tests (vitest): *.test.ts, *.spec.ts
1081
1083
  4. Runs fixture tests: tests/fixture-*.json
1082
1084
 
1085
+ When to use each command:
1086
+ orch validate Quick validation before publishing (config only)
1087
+ orch test Full test suite (config + fixtures + unit tests)
1088
+ orch test --validate-only Same as validate (config only)
1089
+
1083
1090
  Fixture Format (tests/fixture-basic.json):
1084
1091
  {
1085
1092
  "description": "Test description",
@@ -1136,6 +1143,12 @@ Run mode (--run):
1136
1143
  catch {
1137
1144
  // Config not available, fixture tests will use env vars only
1138
1145
  }
1146
+ // --validate-only flag: run validation then exit
1147
+ if (options.validateOnly) {
1148
+ const validation = await validateAgent(agentDir);
1149
+ const isValid = printValidation(validation);
1150
+ process.exit(isValid ? 0 : 1);
1151
+ }
1139
1152
  // Run mode: validate then execute once
1140
1153
  if (options.run) {
1141
1154
  if (!options.data) {
@@ -41,7 +41,7 @@ function printResult(result, serverIssues) {
41
41
  // Header
42
42
  const label = m.isSkill ? 'skill' : 'agent';
43
43
  const name = m.agentName || '(unknown)';
44
- process.stderr.write(`\nValidating ${label}: ${chalk_1.default.bold(name)}\n\n`);
44
+ process.stderr.write(`\n${chalk_1.default.dim('[validation only]')} Validating ${label}: ${chalk_1.default.bold(name)}\n\n`);
45
45
  // Summary line: type, engine, mode
46
46
  if (!m.isSkill && m.agentType && m.executionEngine) {
47
47
  process.stderr.write(chalk_1.default.dim(` Type: ${m.agentType} (${m.executionEngine}), Run mode: ${m.runMode || 'on_demand'}\n\n`));
@@ -115,7 +115,18 @@ function registerValidateCommand(program) {
115
115
  program
116
116
  .command('validate')
117
117
  .alias('lint')
118
- .description('Validate agent or skill configuration without publishing')
118
+ .description('Validate configuration only (no tests)')
119
+ .addHelpText('after', `
120
+ Use 'orch validate' to check configuration before publishing.
121
+ Use 'orch test' to validate + run test suite (fixtures, unit tests, etc).
122
+
123
+ Options:
124
+ --json Output as JSON (for CI/CD pipelines)
125
+ --server Also validate against server (requires auth)
126
+ --profile <name> Use API key from named profile
127
+ --url <url> Agent URL (for code-based agents)
128
+ --docker Validate with Dockerfile
129
+ `)
119
130
  .option('--profile <name>', 'Use API key from named profile')
120
131
  .option('--json', 'Output as JSON (for CI/CD)')
121
132
  .option('--server', 'Also run server-side validation (requires auth)')
@@ -172,6 +172,10 @@ function printHumanOutput(results, summary, verbose) {
172
172
  summaryParts.push(chalk_1.default.red(`${summary.errors} error${summary.errors > 1 ? 's' : ''}`));
173
173
  }
174
174
  process.stdout.write(`Summary: ${summaryParts.join(', ')}\n`);
175
+ // Hint about verbose flag if not already verbose
176
+ if (!verbose) {
177
+ process.stdout.write(chalk_1.default.dim('\nTip: Run with --verbose for detailed information about each check.\n'));
178
+ }
175
179
  }
176
180
  /**
177
181
  * Format results as JSON output.
@@ -53,6 +53,27 @@ class CliError extends Error {
53
53
  }
54
54
  }
55
55
  exports.CliError = CliError;
56
+ function dedupeErrorDetail(message, detail) {
57
+ const normalizedMessage = message.replace(/\r\n/g, '\n').trim();
58
+ const normalizedDetail = detail.replace(/\r\n/g, '\n').trim();
59
+ if (!normalizedDetail)
60
+ return null;
61
+ if (!normalizedMessage)
62
+ return normalizedDetail;
63
+ // Some responses repeat the same text in both `message` and `detail`.
64
+ if (normalizedDetail === normalizedMessage)
65
+ return null;
66
+ // If detail starts with the same first line, keep only the additional context.
67
+ const detailLines = normalizedDetail.split('\n');
68
+ if (detailLines[0] === normalizedMessage) {
69
+ const remainder = detailLines.slice(1).join('\n').trim();
70
+ return remainder || null;
71
+ }
72
+ // No useful extra information.
73
+ if (normalizedMessage.includes(normalizedDetail))
74
+ return null;
75
+ return normalizedDetail;
76
+ }
56
77
  function formatError(err) {
57
78
  if (err instanceof CliError) {
58
79
  return err.message;
@@ -64,8 +85,11 @@ function formatError(err) {
64
85
  const code = p.error?.code;
65
86
  const detail = p.error?.detail || p.detail;
66
87
  let msg = `${anyErr.message} (status ${anyErr.status}${code ? `, ${code}` : ''})`;
67
- if (detail)
68
- msg += `\n${detail}`;
88
+ if (typeof detail === 'string') {
89
+ const dedupedDetail = dedupeErrorDetail(anyErr.message, detail);
90
+ if (dedupedDetail)
91
+ msg += `\n${dedupedDetail}`;
92
+ }
69
93
  return msg;
70
94
  }
71
95
  return anyErr.message;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orchagent/cli",
3
- "version": "0.3.94",
3
+ "version": "0.3.96",
4
4
  "description": "Command-line interface for orchagent — deploy and run AI agents for your team",
5
5
  "license": "MIT",
6
6
  "author": "orchagent <hello@orchagent.io>",