@orchagent/cli 0.3.95 → 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>`.');
@@ -1915,7 +1915,8 @@ async function executeCloud(agentRef, file, options) {
1915
1915
  }
1916
1916
  }
1917
1917
  // --estimate: show cost estimate before running and ask for confirmation
1918
- if (options.estimate) {
1918
+ // --estimate-only: show cost estimate and exit without running
1919
+ if (options.estimate || options.estimateOnly) {
1919
1920
  try {
1920
1921
  const est = await (0, api_1.getAgentCostEstimate)(resolved, org, parsed.agent, parsed.version);
1921
1922
  const e = est.estimate;
@@ -1941,7 +1942,11 @@ async function executeCloud(agentRef, file, options) {
1941
1942
  }
1942
1943
  process.stderr.write('\n');
1943
1944
  }
1944
- // 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)
1945
1950
  const rl = await Promise.resolve().then(() => __importStar(require('readline')));
1946
1951
  const iface = rl.createInterface({ input: process.stdin, output: process.stderr });
1947
1952
  const answer = await new Promise(resolve => {
@@ -1954,7 +1959,10 @@ async function executeCloud(agentRef, file, options) {
1954
1959
  }
1955
1960
  }
1956
1961
  catch {
1957
- // 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
+ }
1958
1966
  process.stderr.write(chalk_1.default.gray('Could not fetch cost estimate. Proceeding...\n\n'));
1959
1967
  }
1960
1968
  }
@@ -2879,6 +2887,7 @@ function registerRunCommand(program) {
2879
2887
  .option('--json', 'Output raw JSON')
2880
2888
  .option('--verbose', 'Show sandbox stdout/stderr and debug info (cloud only)')
2881
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)')
2882
2891
  .option('--provider <provider>', 'LLM provider (openai, anthropic, gemini, ollama)')
2883
2892
  .option('--model <model>', 'LLM model to use (overrides agent default)')
2884
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)')
@@ -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.95",
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>",