@formigio/fazemos-cli 0.5.0 → 0.6.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.
- package/dist/index.js +393 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { config, getEnv, getToken, getActiveOrgId, setActiveOrgId, addEnvironmen
|
|
|
5
5
|
import { login, signup, confirmSignup, adminLogin } from './auth.js';
|
|
6
6
|
import { api, ApiError } from './api.js';
|
|
7
7
|
import { execSync } from 'child_process';
|
|
8
|
-
import { readFileSync, readdirSync } from 'fs';
|
|
8
|
+
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
9
9
|
import { fileURLToPath } from 'url';
|
|
10
10
|
import { dirname, resolve, basename } from 'path';
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -25,6 +25,58 @@ function parseJson(val, flag) {
|
|
|
25
25
|
process.exit(1);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
/** Resolve a value that may be `@filepath` (reads file contents) or inline text. */
|
|
29
|
+
function resolveFileOrInline(val) {
|
|
30
|
+
if (val.startsWith('@')) {
|
|
31
|
+
const filePath = resolve(val.slice(1));
|
|
32
|
+
try {
|
|
33
|
+
return readFileSync(filePath, 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
throw new Error(`Cannot read --sections file "${filePath}": ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return val;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Make a string safe to use as a filename: strip path separators and other
|
|
43
|
+
* shell-hostile characters, reject `..` traversal. Used by `agents export` so
|
|
44
|
+
* an agent display_name like `foo/bar` or `..` cannot escape the output dir.
|
|
45
|
+
*/
|
|
46
|
+
function safeFileName(name) {
|
|
47
|
+
const cleaned = name.replace(/[^\w.\- ]+/g, '_').trim();
|
|
48
|
+
if (!cleaned || cleaned === '.' || cleaned === '..') {
|
|
49
|
+
throw new Error(`Cannot derive safe filename from "${name}"`);
|
|
50
|
+
}
|
|
51
|
+
return cleaned;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the markdown content for an exported agent. With `includeFrontmatter`,
|
|
55
|
+
* prepends YAML frontmatter reconstructed from `agent_config`. Note: only
|
|
56
|
+
* `name` round-trips through `upload-all` today — the other fields are
|
|
57
|
+
* informational on import. String values are JSON-stringified to keep YAML
|
|
58
|
+
* valid even if a model name or role contains a colon.
|
|
59
|
+
*/
|
|
60
|
+
function buildAgentFileContent(agent, includeFrontmatter) {
|
|
61
|
+
const prompt = agent.agent_config?.systemPrompt || '';
|
|
62
|
+
if (!includeFrontmatter)
|
|
63
|
+
return prompt + '\n';
|
|
64
|
+
const ac = agent.agent_config || {};
|
|
65
|
+
const fm = ['---'];
|
|
66
|
+
fm.push(`name: ${JSON.stringify(agent.display_name)}`);
|
|
67
|
+
if (ac.model)
|
|
68
|
+
fm.push(`model: ${JSON.stringify(ac.model)}`);
|
|
69
|
+
if (ac.maxBudgetUsd != null)
|
|
70
|
+
fm.push(`maxBudgetUsd: ${ac.maxBudgetUsd}`);
|
|
71
|
+
if (ac.maxTurns != null)
|
|
72
|
+
fm.push(`maxTurns: ${ac.maxTurns}`);
|
|
73
|
+
if (ac.timeoutMs != null)
|
|
74
|
+
fm.push(`timeoutMs: ${ac.timeoutMs}`);
|
|
75
|
+
if (ac.roles?.length)
|
|
76
|
+
fm.push(`roles: ${JSON.stringify(ac.roles)}`);
|
|
77
|
+
fm.push('---');
|
|
78
|
+
return fm.join('\n') + '\n\n' + prompt + '\n';
|
|
79
|
+
}
|
|
28
80
|
const program = new Command();
|
|
29
81
|
program
|
|
30
82
|
.name('fazemos')
|
|
@@ -653,6 +705,149 @@ invites
|
|
|
653
705
|
process.exit(1);
|
|
654
706
|
}
|
|
655
707
|
});
|
|
708
|
+
// ── orgs notifications subcommands (F12) ────────────────────
|
|
709
|
+
const NOTIFICATION_EVENT_TYPES = [
|
|
710
|
+
'agent_started',
|
|
711
|
+
'agent_completed',
|
|
712
|
+
'agent_failed',
|
|
713
|
+
'human_work',
|
|
714
|
+
'pipeline_complete',
|
|
715
|
+
];
|
|
716
|
+
const notifications = orgs
|
|
717
|
+
.command('notifications')
|
|
718
|
+
.description(`Manage Slack notifications for the active org.
|
|
719
|
+
|
|
720
|
+
Notifications are sent to a Slack incoming webhook URL configured per org.
|
|
721
|
+
Default events: agent_failed, human_work, pipeline_complete (loud events
|
|
722
|
+
only). agent_started and agent_completed are off by default to keep the
|
|
723
|
+
channel quiet.
|
|
724
|
+
|
|
725
|
+
The webhook URL is stored on the API side and never returned by the API.
|
|
726
|
+
To rotate the URL, regenerate it in Slack and call set-webhook again.
|
|
727
|
+
Owner role required to change the webhook URL; admin can toggle events.`);
|
|
728
|
+
function requireActiveOrgOrExit() {
|
|
729
|
+
const orgId = getActiveOrgId();
|
|
730
|
+
if (!orgId) {
|
|
731
|
+
console.error(chalk.red('No active org. Run: fazemos orgs switch <slug>'));
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
return orgId;
|
|
735
|
+
}
|
|
736
|
+
notifications
|
|
737
|
+
.command('get')
|
|
738
|
+
.description('Show current notification config (events enabled, webhook source)')
|
|
739
|
+
.action(async () => {
|
|
740
|
+
try {
|
|
741
|
+
const orgId = requireActiveOrgOrExit();
|
|
742
|
+
const data = await api('GET', `/api/organizations/${orgId}/notifications/config`);
|
|
743
|
+
console.log(chalk.cyan('Notifications config:'));
|
|
744
|
+
console.log(` Webhook configured: ${data.webhook_configured ? chalk.green('yes') : chalk.gray('no')}`);
|
|
745
|
+
console.log(` Webhook source: ${data.webhook_source}`);
|
|
746
|
+
console.log(' Events:');
|
|
747
|
+
for (const ev of NOTIFICATION_EVENT_TYPES) {
|
|
748
|
+
const on = data.events_enabled[ev];
|
|
749
|
+
console.log(` ${on ? chalk.green('●') : chalk.gray('○')} ${ev}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
console.error(chalk.red(err.message));
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
notifications
|
|
758
|
+
.command('set-webhook')
|
|
759
|
+
.description('Set the Slack incoming webhook URL for the active org. Owner only.')
|
|
760
|
+
.requiredOption('-u, --url <url>', 'Slack incoming webhook URL (https://hooks.slack.com/services/...)')
|
|
761
|
+
.action(async (opts) => {
|
|
762
|
+
try {
|
|
763
|
+
const orgId = requireActiveOrgOrExit();
|
|
764
|
+
const data = await api('PUT', `/api/organizations/${orgId}/notifications/config`, { webhook_url: opts.url });
|
|
765
|
+
console.log(chalk.green('Webhook updated.'));
|
|
766
|
+
console.log(` Source: ${data.webhook_source}`);
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
console.error(chalk.red(err.message));
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
notifications
|
|
774
|
+
.command('clear-webhook')
|
|
775
|
+
.description('Remove the Slack webhook URL for the active org. Notifications stop being sent. Owner only.')
|
|
776
|
+
.action(async () => {
|
|
777
|
+
try {
|
|
778
|
+
const orgId = requireActiveOrgOrExit();
|
|
779
|
+
await api('PUT', `/api/organizations/${orgId}/notifications/config`, { webhook_url: null });
|
|
780
|
+
console.log(chalk.green('Webhook cleared. Notifications are now silent until a new webhook is configured.'));
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
console.error(chalk.red(err.message));
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
notifications
|
|
788
|
+
.command('enable')
|
|
789
|
+
.description('Enable a notification event type for the active org')
|
|
790
|
+
.argument('<event-type>', `One of: ${NOTIFICATION_EVENT_TYPES.join(', ')}`)
|
|
791
|
+
.action(async (eventType) => {
|
|
792
|
+
try {
|
|
793
|
+
if (!NOTIFICATION_EVENT_TYPES.includes(eventType)) {
|
|
794
|
+
console.error(chalk.red(`Unknown event type: ${eventType}`));
|
|
795
|
+
console.error(`Valid: ${NOTIFICATION_EVENT_TYPES.join(', ')}`);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
const orgId = requireActiveOrgOrExit();
|
|
799
|
+
const current = await api('GET', `/api/organizations/${orgId}/notifications/config`);
|
|
800
|
+
const events_enabled = { ...current.events_enabled, [eventType]: true };
|
|
801
|
+
await api('PUT', `/api/organizations/${orgId}/notifications/config`, { events_enabled });
|
|
802
|
+
console.log(chalk.green(`Enabled: ${eventType}`));
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
console.error(chalk.red(err.message));
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
notifications
|
|
810
|
+
.command('disable')
|
|
811
|
+
.description('Disable a notification event type for the active org')
|
|
812
|
+
.argument('<event-type>', `One of: ${NOTIFICATION_EVENT_TYPES.join(', ')}`)
|
|
813
|
+
.action(async (eventType) => {
|
|
814
|
+
try {
|
|
815
|
+
if (!NOTIFICATION_EVENT_TYPES.includes(eventType)) {
|
|
816
|
+
console.error(chalk.red(`Unknown event type: ${eventType}`));
|
|
817
|
+
console.error(`Valid: ${NOTIFICATION_EVENT_TYPES.join(', ')}`);
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
const orgId = requireActiveOrgOrExit();
|
|
821
|
+
const current = await api('GET', `/api/organizations/${orgId}/notifications/config`);
|
|
822
|
+
const events_enabled = { ...current.events_enabled, [eventType]: false };
|
|
823
|
+
await api('PUT', `/api/organizations/${orgId}/notifications/config`, { events_enabled });
|
|
824
|
+
console.log(chalk.green(`Disabled: ${eventType}`));
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
console.error(chalk.red(err.message));
|
|
828
|
+
process.exit(1);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
notifications
|
|
832
|
+
.command('test')
|
|
833
|
+
.description('Send a test notification to verify the configured webhook is working')
|
|
834
|
+
.action(async () => {
|
|
835
|
+
try {
|
|
836
|
+
const orgId = requireActiveOrgOrExit();
|
|
837
|
+
const result = await api('POST', `/api/organizations/${orgId}/notifications/test`, {});
|
|
838
|
+
if (result.success) {
|
|
839
|
+
console.log(chalk.green(`Test notification sent via ${result.channel}. Check your Slack channel.`));
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
console.error(chalk.red(`Test failed (${result.channel}): ${result.error}`));
|
|
843
|
+
process.exit(1);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch (err) {
|
|
847
|
+
console.error(chalk.red(err.message));
|
|
848
|
+
process.exit(1);
|
|
849
|
+
}
|
|
850
|
+
});
|
|
656
851
|
// ── Worksheets ──────────────────────────────────────────────
|
|
657
852
|
const ws = program.command('worksheets').alias('ws').description('Worksheet commands');
|
|
658
853
|
ws
|
|
@@ -1727,6 +1922,16 @@ function allSteps(definition) {
|
|
|
1727
1922
|
function findStepById(definition, stepId) {
|
|
1728
1923
|
return allSteps(definition).find((s) => s.id === stepId);
|
|
1729
1924
|
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Pipeline-sourced step inputs went through a schema migration: the old
|
|
1927
|
+
* shape stored the pipeline input name under `pipeline_input`; the new shape
|
|
1928
|
+
* uses `source: "pipeline"` + `source_pipeline_input`. Read sites should call
|
|
1929
|
+
* this helper instead of touching either field directly so both shapes work.
|
|
1930
|
+
* Returns undefined for step-sourced inputs.
|
|
1931
|
+
*/
|
|
1932
|
+
function getPipelineInputName(input) {
|
|
1933
|
+
return input?.source_pipeline_input ?? input?.pipeline_input;
|
|
1934
|
+
}
|
|
1730
1935
|
const VALID_IO_TYPES = ['text', 'markdown', 'number', 'boolean', 'url', 'json', 'object', 'array'];
|
|
1731
1936
|
function requireDraftStatus(template) {
|
|
1732
1937
|
if (template.status !== 'draft') {
|
|
@@ -1750,6 +1955,7 @@ const templates = program.command('templates').alias('tpl').description('Pipelin
|
|
|
1750
1955
|
' 4. tpl add-output / add-input Wire I/O between steps\n' +
|
|
1751
1956
|
' 5. tpl validate <id> Check for errors\n' +
|
|
1752
1957
|
' 6. tpl activate <id> Make available for instances\n\n' +
|
|
1958
|
+
' Use "tpl phases <id>" to list phase IDs needed by --phase options.\n' +
|
|
1753
1959
|
' Use "tpl steps <id>" to list step IDs needed by --step options.\n' +
|
|
1754
1960
|
' Use "tpl show <id>" to see full structure with I/O declarations.');
|
|
1755
1961
|
templates
|
|
@@ -1777,7 +1983,7 @@ templates
|
|
|
1777
1983
|
});
|
|
1778
1984
|
templates
|
|
1779
1985
|
.command('show')
|
|
1780
|
-
.description('Show template detail including phases, steps, I/O declarations, and
|
|
1986
|
+
.description('Show template detail including phases, steps, I/O declarations, pipeline inputs, and revision number. Use this to inspect the full structure of a template and discover phase/step IDs needed by other commands.')
|
|
1781
1987
|
.argument('<id>', 'Template ID (use "tpl list" to find IDs)')
|
|
1782
1988
|
.action(async (id) => {
|
|
1783
1989
|
try {
|
|
@@ -1786,7 +1992,7 @@ templates
|
|
|
1786
1992
|
console.log(chalk.cyan(t.name));
|
|
1787
1993
|
console.log(` ID: ${t.id}`);
|
|
1788
1994
|
console.log(` Status: ${t.status}`);
|
|
1789
|
-
console.log(`
|
|
1995
|
+
console.log(` Revision: ${t.version}`);
|
|
1790
1996
|
// Pipeline-level inputs
|
|
1791
1997
|
if (t.definition?.inputs?.length) {
|
|
1792
1998
|
console.log(chalk.cyan('\n Pipeline Inputs:'));
|
|
@@ -1799,7 +2005,7 @@ templates
|
|
|
1799
2005
|
}
|
|
1800
2006
|
if (t.definition?.phases) {
|
|
1801
2007
|
for (const phase of t.definition.phases) {
|
|
1802
|
-
console.log(chalk.cyan(`\n Phase: ${phase.name}`));
|
|
2008
|
+
console.log(chalk.cyan(`\n Phase: ${phase.name}`) + chalk.dim(` ${phase.id}`));
|
|
1803
2009
|
for (const step of phase.steps || []) {
|
|
1804
2010
|
const reviewer = step.reviewer ? ` reviewer:${step.reviewer}` : '';
|
|
1805
2011
|
const cycles = step.reviewer && step.max_review_cycles ? ` max-cycles:${step.max_review_cycles}` : '';
|
|
@@ -1815,8 +2021,9 @@ templates
|
|
|
1815
2021
|
if (step.inputs?.length) {
|
|
1816
2022
|
console.log(' Inputs:');
|
|
1817
2023
|
for (const inp of step.inputs) {
|
|
1818
|
-
|
|
1819
|
-
|
|
2024
|
+
const pName = getPipelineInputName(inp);
|
|
2025
|
+
if (pName) {
|
|
2026
|
+
console.log(` ← ${inp.name} ← pipeline.${pName}`);
|
|
1820
2027
|
}
|
|
1821
2028
|
else {
|
|
1822
2029
|
const srcStep = findStepById(t.definition, inp.source_step_id);
|
|
@@ -1921,13 +2128,23 @@ templates
|
|
|
1921
2128
|
description: s.description || '',
|
|
1922
2129
|
step_type: s.executionMode === 'script' ? 'script' : (s.agent ? 'agent' : 'human'),
|
|
1923
2130
|
role: s.role || s.agent || 'unassigned',
|
|
1924
|
-
inputs: (s.inputs || []).map((inp) =>
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2131
|
+
inputs: (s.inputs || []).map((inp) => {
|
|
2132
|
+
const pipelineName = getPipelineInputName(inp);
|
|
2133
|
+
return {
|
|
2134
|
+
name: inp.name,
|
|
2135
|
+
...(inp.source_step_id ? {
|
|
2136
|
+
source: 'step',
|
|
2137
|
+
source_step_id: idMap.get(inp.source_step_id) || inp.source_step_id,
|
|
2138
|
+
source_output_name: inp.source_output_name,
|
|
2139
|
+
} : {}),
|
|
2140
|
+
...(pipelineName ? {
|
|
2141
|
+
source: 'pipeline',
|
|
2142
|
+
source_pipeline_input: pipelineName,
|
|
2143
|
+
} : {}),
|
|
2144
|
+
...(inp.description ? { description: inp.description } : {}),
|
|
2145
|
+
required: inp.required !== false,
|
|
2146
|
+
};
|
|
2147
|
+
}),
|
|
1931
2148
|
outputs: (s.outputs || []).map((o) => ({
|
|
1932
2149
|
name: o.name,
|
|
1933
2150
|
type: o.type || 'text',
|
|
@@ -1995,7 +2212,7 @@ templates
|
|
|
1995
2212
|
// ── Template structure commands ─────────────────────────────
|
|
1996
2213
|
templates
|
|
1997
2214
|
.command('update')
|
|
1998
|
-
.description('Update template name or description. Works on any status (draft, active, or archived). Does not modify the definition or bump
|
|
2215
|
+
.description('Update template name or description. Works on any status (draft, active, or archived). Does not modify the definition or bump the revision counter.')
|
|
1999
2216
|
.argument('<id>', 'Template ID')
|
|
2000
2217
|
.option('-n, --name <name>', 'New name')
|
|
2001
2218
|
.option('-d, --description <desc>', 'New description')
|
|
@@ -2082,9 +2299,9 @@ templates
|
|
|
2082
2299
|
});
|
|
2083
2300
|
templates
|
|
2084
2301
|
.command('remove-phase')
|
|
2085
|
-
.description('Remove a phase from a template. Blocked if the phase contains steps unless --force is used. Template must be in draft status. Use "tpl show" to find phase IDs.')
|
|
2302
|
+
.description('Remove a phase from a template. Blocked if the phase contains steps unless --force is used. Template must be in draft status. Use "tpl phases" or "tpl show" to find phase IDs.')
|
|
2086
2303
|
.argument('<templateId>', 'Template ID')
|
|
2087
|
-
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" output)')
|
|
2304
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl phases" or "tpl show" output)')
|
|
2088
2305
|
.option('--force', 'Remove even if phase contains steps')
|
|
2089
2306
|
.action(async (templateId, opts) => {
|
|
2090
2307
|
try {
|
|
@@ -2113,9 +2330,9 @@ templates
|
|
|
2113
2330
|
});
|
|
2114
2331
|
templates
|
|
2115
2332
|
.command('edit-phase')
|
|
2116
|
-
.description('Edit a phase name or description. Template must be in draft status. Provide at least one of --name or --description.')
|
|
2333
|
+
.description('Edit a phase name or description. Template must be in draft status. Provide at least one of --name or --description. Use "tpl phases" to find phase IDs.')
|
|
2117
2334
|
.argument('<templateId>', 'Template ID')
|
|
2118
|
-
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" output)')
|
|
2335
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl phases" or "tpl show" output)')
|
|
2119
2336
|
.option('--name <name>', 'New phase name (must be unique within the template)')
|
|
2120
2337
|
.option('--description <desc>', 'New phase description')
|
|
2121
2338
|
.action(async (templateId, opts) => {
|
|
@@ -2151,9 +2368,9 @@ templates
|
|
|
2151
2368
|
});
|
|
2152
2369
|
templates
|
|
2153
2370
|
.command('add-step')
|
|
2154
|
-
.description('Add a step to a phase. Template must be in draft status. Step names must be unique within their phase. Returns the generated step ID needed by I/O commands (add-output, add-input, etc.). Use "tpl show" or "tpl steps" to find existing step IDs.')
|
|
2371
|
+
.description('Add a step to a phase. Template must be in draft status. Step names must be unique within their phase. Returns the generated step ID needed by I/O commands (add-output, add-input, etc.). Use --sections to provide agent instructions (inline or @filepath). Use --after to control ordering. Use "tpl show" or "tpl steps" to find existing step IDs.')
|
|
2155
2372
|
.argument('<templateId>', 'Template ID')
|
|
2156
|
-
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl show" or "tpl add-phase" output)')
|
|
2373
|
+
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl phases", "tpl show", or "tpl add-phase" output)')
|
|
2157
2374
|
.requiredOption('--name <name>', 'Step name (must be unique within the phase)')
|
|
2158
2375
|
.option('--type <type>', 'Step type: human (manual task), agent (AI agent), script (automated), gate (approval checkpoint)', 'human')
|
|
2159
2376
|
.option('--role <role>', 'Role or agent name (e.g., "kate", "marco", "dev-team")')
|
|
@@ -2166,6 +2383,9 @@ templates
|
|
|
2166
2383
|
.option('--timeout <seconds>', 'Timeout in seconds for script steps', parseNumber)
|
|
2167
2384
|
.option('--working-dir <dir>', 'Working directory for script steps (absolute path)')
|
|
2168
2385
|
.option('--env <json>', 'Environment variables for script steps as JSON (e.g., \'{"KEY":"value"}\')')
|
|
2386
|
+
.option('--sections <text>', 'Agent instructions / step content. Use @filepath to load from a file (e.g., --sections @steps/review.md)')
|
|
2387
|
+
.option('--after <stepId>', 'Insert after this step within the same phase (sets sort_order to target + 1, shifts subsequent steps). Target must live in the phase named by --phase.')
|
|
2388
|
+
.option('--sort-order <n>', 'Set sort_order directly (lower-level escape hatch)', parseNumber)
|
|
2169
2389
|
.option('--agent-config <json>', 'Per-step agent config overrides as JSON (e.g., \'{"model":"opus","maxBudgetUsd":20}\'). Overrides agent member defaults for: model, maxBudgetUsd, maxTurns, timeoutMs, cwd, repos')
|
|
2170
2390
|
.action(async (templateId, opts) => {
|
|
2171
2391
|
try {
|
|
@@ -2206,6 +2426,9 @@ templates
|
|
|
2206
2426
|
execConfig.env = parseJson(opts.env, '--env');
|
|
2207
2427
|
}
|
|
2208
2428
|
}
|
|
2429
|
+
// Default sort_order: max existing + 1, so non-contiguous gaps don't
|
|
2430
|
+
// cause new steps to collide with existing ones (e.g. after a --after insert).
|
|
2431
|
+
const maxSortOrder = phase.steps.reduce((m, s) => Math.max(m, s.sort_order ?? 0), -1);
|
|
2209
2432
|
const step = {
|
|
2210
2433
|
id: crypto.randomUUID(),
|
|
2211
2434
|
name: opts.name,
|
|
@@ -2214,16 +2437,37 @@ templates
|
|
|
2214
2437
|
role: opts.role || 'unassigned',
|
|
2215
2438
|
inputs: [],
|
|
2216
2439
|
outputs: [],
|
|
2217
|
-
sections: '',
|
|
2440
|
+
sections: opts.sections ? resolveFileOrInline(opts.sections) : '',
|
|
2218
2441
|
reviewer: opts.reviewer || null,
|
|
2219
2442
|
max_review_cycles: parseInt(opts.maxReviewCycles) || 0,
|
|
2220
2443
|
execution_config: execConfig,
|
|
2221
2444
|
parallel_group: opts.parallelGroup || null,
|
|
2222
|
-
sort_order:
|
|
2445
|
+
sort_order: maxSortOrder + 1,
|
|
2223
2446
|
};
|
|
2224
2447
|
if (opts.agentConfig)
|
|
2225
2448
|
step.agent_config = parseJson(opts.agentConfig, '--agent-config');
|
|
2449
|
+
// Positioning: --after or --sort-order
|
|
2450
|
+
if (opts.after && opts.sortOrder !== undefined) {
|
|
2451
|
+
console.error(chalk.red('Cannot use both --after and --sort-order'));
|
|
2452
|
+
process.exit(1);
|
|
2453
|
+
}
|
|
2454
|
+
if (opts.after) {
|
|
2455
|
+
const target = phase.steps.find((s) => s.id === opts.after);
|
|
2456
|
+
if (!target) {
|
|
2457
|
+
console.error(chalk.red(`Step "${opts.after}" not found in phase "${phase.name}"`));
|
|
2458
|
+
process.exit(1);
|
|
2459
|
+
}
|
|
2460
|
+
step.sort_order = target.sort_order + 1;
|
|
2461
|
+
for (const s of phase.steps) {
|
|
2462
|
+
if (s.sort_order >= step.sort_order)
|
|
2463
|
+
s.sort_order++;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
else if (opts.sortOrder !== undefined) {
|
|
2467
|
+
step.sort_order = opts.sortOrder;
|
|
2468
|
+
}
|
|
2226
2469
|
phase.steps.push(step);
|
|
2470
|
+
phase.steps.sort((a, b) => a.sort_order - b.sort_order);
|
|
2227
2471
|
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
2228
2472
|
console.log(chalk.green(`Added step: ${opts.name} (${opts.type}) to phase ${phase.name}`));
|
|
2229
2473
|
console.log(` ID: ${step.id}`);
|
|
@@ -2278,7 +2522,7 @@ templates
|
|
|
2278
2522
|
});
|
|
2279
2523
|
templates
|
|
2280
2524
|
.command('edit-step')
|
|
2281
|
-
.description('Edit step properties. Template must be in draft status. Provide at least one field to update. Use "tpl steps" to find step IDs.')
|
|
2525
|
+
.description('Edit step properties including sections (agent instructions). Template must be in draft status. Provide at least one field to update. Use "tpl steps" to find step IDs.')
|
|
2282
2526
|
.argument('<templateId>', 'Template ID')
|
|
2283
2527
|
.requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
|
|
2284
2528
|
.option('--name <name>', 'New step name (must be unique within the phase)')
|
|
@@ -2293,13 +2537,14 @@ templates
|
|
|
2293
2537
|
.option('--timeout <seconds>', 'Timeout in seconds for script steps', parseNumber)
|
|
2294
2538
|
.option('--working-dir <dir>', 'Working directory for script steps (absolute path)')
|
|
2295
2539
|
.option('--env <json>', 'Environment variables for script steps as JSON')
|
|
2540
|
+
.option('--sections <text>', 'Agent instructions / step content. Use @filepath to load from a file (e.g., --sections @steps/review.md)')
|
|
2296
2541
|
.option('--agent-config <json>', 'Per-step agent config overrides as JSON (merges with existing)')
|
|
2297
2542
|
.action(async (templateId, opts) => {
|
|
2298
2543
|
try {
|
|
2299
2544
|
const hasUpdate = opts.name || opts.type || opts.role || opts.description != null
|
|
2300
2545
|
|| opts.reviewer != null || opts.maxReviewCycles != null || opts.parallelGroup != null
|
|
2301
2546
|
|| opts.image || opts.command || opts.timeout !== undefined || opts.workingDir || opts.env
|
|
2302
|
-
|| opts.agentConfig;
|
|
2547
|
+
|| opts.agentConfig || opts.sections != null;
|
|
2303
2548
|
if (!hasUpdate) {
|
|
2304
2549
|
console.error(chalk.red('Provide at least one field to update'));
|
|
2305
2550
|
process.exit(1);
|
|
@@ -2361,6 +2606,9 @@ templates
|
|
|
2361
2606
|
step.execution_config.env = parseJson(opts.env, '--env');
|
|
2362
2607
|
}
|
|
2363
2608
|
}
|
|
2609
|
+
// sections (agent instructions)
|
|
2610
|
+
if (opts.sections != null)
|
|
2611
|
+
step.sections = resolveFileOrInline(opts.sections);
|
|
2364
2612
|
// agent_config overrides (merge with existing)
|
|
2365
2613
|
if (opts.agentConfig) {
|
|
2366
2614
|
step.agent_config = { ...(step.agent_config || {}), ...parseJson(opts.agentConfig, '--agent-config') };
|
|
@@ -2374,6 +2622,28 @@ templates
|
|
|
2374
2622
|
}
|
|
2375
2623
|
});
|
|
2376
2624
|
// ── Template I/O commands ──────────────────────────────────
|
|
2625
|
+
templates
|
|
2626
|
+
.command('phases')
|
|
2627
|
+
.description('List phase IDs and names in a template. Use this to discover phase IDs needed by --phase options in add-step, edit-phase, remove-phase, etc.')
|
|
2628
|
+
.argument('<id>', 'Template ID')
|
|
2629
|
+
.action(async (id) => {
|
|
2630
|
+
try {
|
|
2631
|
+
const data = await api('GET', `/api/pipeline-templates/${id}`);
|
|
2632
|
+
const phases = data.template.definition?.phases || [];
|
|
2633
|
+
if (!phases.length) {
|
|
2634
|
+
console.log(chalk.yellow('No phases'));
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
for (const p of phases) {
|
|
2638
|
+
const stepCount = (p.steps || []).length;
|
|
2639
|
+
console.log(` ${chalk.dim(p.id)} ${p.name} ${chalk.dim(`(${stepCount} steps)`)}`);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
catch (err) {
|
|
2643
|
+
console.error(chalk.red(err.message));
|
|
2644
|
+
process.exit(1);
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2377
2647
|
templates
|
|
2378
2648
|
.command('steps')
|
|
2379
2649
|
.description('List step IDs and names in a template. Use this to discover step IDs needed by --step options in add-output, add-input, remove-step, edit-step, etc.')
|
|
@@ -2547,11 +2817,13 @@ templates
|
|
|
2547
2817
|
}
|
|
2548
2818
|
const input = { name: opts.name, required: !opts.optional };
|
|
2549
2819
|
if (opts.sourceStep) {
|
|
2820
|
+
input.source = 'step';
|
|
2550
2821
|
input.source_step_id = opts.sourceStep;
|
|
2551
2822
|
input.source_output_name = opts.sourceOutput;
|
|
2552
2823
|
}
|
|
2553
2824
|
else {
|
|
2554
|
-
input.
|
|
2825
|
+
input.source = 'pipeline';
|
|
2826
|
+
input.source_pipeline_input = opts.pipelineInput;
|
|
2555
2827
|
}
|
|
2556
2828
|
if (opts.description)
|
|
2557
2829
|
input.description = opts.description;
|
|
@@ -2656,7 +2928,7 @@ templates
|
|
|
2656
2928
|
process.exit(1);
|
|
2657
2929
|
}
|
|
2658
2930
|
// Block if steps reference it unless --force
|
|
2659
|
-
const refs = allSteps(t.definition).filter((s) => (s.inputs || []).some((inp) => inp
|
|
2931
|
+
const refs = allSteps(t.definition).filter((s) => (s.inputs || []).some((inp) => getPipelineInputName(inp) === opts.name));
|
|
2660
2932
|
if (refs.length && !opts.force) {
|
|
2661
2933
|
console.error(chalk.yellow(`Pipeline input "${opts.name}" is referenced by: ${refs.map((r) => r.name).join(', ')}`));
|
|
2662
2934
|
console.error(chalk.yellow('Use --force to remove anyway'));
|
|
@@ -2730,9 +3002,10 @@ templates
|
|
|
2730
3002
|
errors++;
|
|
2731
3003
|
}
|
|
2732
3004
|
}
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
3005
|
+
const pipelineName = getPipelineInputName(inp);
|
|
3006
|
+
if (pipelineName) {
|
|
3007
|
+
if (!pipelineInputs.find((p) => p.name === pipelineName)) {
|
|
3008
|
+
console.log(chalk.red(` ✗ Step "${step.name}" input "${inp.name}" references missing pipeline input "${pipelineName}"`));
|
|
2736
3009
|
hasUnresolved = true;
|
|
2737
3010
|
errors++;
|
|
2738
3011
|
}
|
|
@@ -4285,6 +4558,97 @@ agentsCmd
|
|
|
4285
4558
|
process.exit(1);
|
|
4286
4559
|
}
|
|
4287
4560
|
});
|
|
4561
|
+
agentsCmd
|
|
4562
|
+
.command('export')
|
|
4563
|
+
.description('Export all agent system prompts to a directory. Default: body only (matches what upload-all consumes). Use --include-frontmatter to also write readable YAML frontmatter — note that upload-all currently only reads the `name:` field, so the other frontmatter fields are informational.')
|
|
4564
|
+
.argument('<output-dir>', 'Directory to write agent .md files to (created if needed)')
|
|
4565
|
+
.option('--include-frontmatter', 'Include YAML frontmatter with agent config fields (name, model, maxBudgetUsd, maxTurns, timeoutMs, roles)')
|
|
4566
|
+
.action(async (outputDir, opts) => {
|
|
4567
|
+
try {
|
|
4568
|
+
const orgId = getActiveOrgId();
|
|
4569
|
+
if (!orgId) {
|
|
4570
|
+
console.error(chalk.red('No active org'));
|
|
4571
|
+
process.exit(1);
|
|
4572
|
+
}
|
|
4573
|
+
const resolvedDir = resolve(outputDir);
|
|
4574
|
+
if (existsSync(resolvedDir) && statSync(resolvedDir).isFile()) {
|
|
4575
|
+
console.error(chalk.red(`"${resolvedDir}" is a file, not a directory`));
|
|
4576
|
+
process.exit(1);
|
|
4577
|
+
}
|
|
4578
|
+
mkdirSync(resolvedDir, { recursive: true });
|
|
4579
|
+
const membersData = await api('GET', `/api/organizations/${orgId}/members?type=agent`);
|
|
4580
|
+
const agents = membersData.members.filter((m) => m.member_type === 'agent');
|
|
4581
|
+
let exported = 0;
|
|
4582
|
+
let skipped = 0;
|
|
4583
|
+
const writtenFiles = new Set();
|
|
4584
|
+
for (const agent of agents) {
|
|
4585
|
+
const prompt = agent.agent_config?.systemPrompt;
|
|
4586
|
+
if (!prompt) {
|
|
4587
|
+
console.log(chalk.yellow(` ⊘ ${agent.display_name} — no systemPrompt`));
|
|
4588
|
+
skipped++;
|
|
4589
|
+
continue;
|
|
4590
|
+
}
|
|
4591
|
+
let fileName;
|
|
4592
|
+
try {
|
|
4593
|
+
fileName = `${safeFileName(agent.display_name)}.md`;
|
|
4594
|
+
}
|
|
4595
|
+
catch (err) {
|
|
4596
|
+
console.log(chalk.yellow(` ⊘ ${agent.display_name} — ${err.message}`));
|
|
4597
|
+
skipped++;
|
|
4598
|
+
continue;
|
|
4599
|
+
}
|
|
4600
|
+
if (writtenFiles.has(fileName)) {
|
|
4601
|
+
console.log(chalk.yellow(` ⊘ ${agent.display_name} — filename "${fileName}" already written by another agent (display name collision)`));
|
|
4602
|
+
skipped++;
|
|
4603
|
+
continue;
|
|
4604
|
+
}
|
|
4605
|
+
const content = buildAgentFileContent(agent, !!opts.includeFrontmatter);
|
|
4606
|
+
writeFileSync(resolve(resolvedDir, fileName), content, 'utf-8');
|
|
4607
|
+
writtenFiles.add(fileName);
|
|
4608
|
+
console.log(chalk.green(` ✓ ${agent.display_name}`));
|
|
4609
|
+
exported++;
|
|
4610
|
+
}
|
|
4611
|
+
console.log(`\n${exported} agents exported to ${resolvedDir}${skipped ? `, ${skipped} skipped` : ''}`);
|
|
4612
|
+
}
|
|
4613
|
+
catch (err) {
|
|
4614
|
+
console.error(chalk.red(err.message));
|
|
4615
|
+
process.exit(1);
|
|
4616
|
+
}
|
|
4617
|
+
});
|
|
4618
|
+
agentsCmd
|
|
4619
|
+
.command('export-definition')
|
|
4620
|
+
.description('Export a single agent system prompt to a file. Matches agent by display name (case-insensitive). Default: body only. Use --include-frontmatter to also write readable YAML frontmatter — note that upload-definition only reads the body, so the frontmatter fields are informational.')
|
|
4621
|
+
.argument('<name>', 'Agent display name')
|
|
4622
|
+
.argument('<file>', 'Output file path')
|
|
4623
|
+
.option('--include-frontmatter', 'Include YAML frontmatter with agent config fields (name, model, maxBudgetUsd, maxTurns, timeoutMs, roles)')
|
|
4624
|
+
.action(async (name, file, opts) => {
|
|
4625
|
+
try {
|
|
4626
|
+
const orgId = getActiveOrgId();
|
|
4627
|
+
if (!orgId) {
|
|
4628
|
+
console.error(chalk.red('No active org'));
|
|
4629
|
+
process.exit(1);
|
|
4630
|
+
}
|
|
4631
|
+
const membersData = await api('GET', `/api/organizations/${orgId}/members?type=agent`);
|
|
4632
|
+
const norm = (s) => s.toLowerCase().replace(/[-_\s]+/g, ' ').trim();
|
|
4633
|
+
const agent = membersData.members.find((m) => norm(m.display_name) === norm(name));
|
|
4634
|
+
if (!agent) {
|
|
4635
|
+
console.error(chalk.red(`Agent "${name}" not found`));
|
|
4636
|
+
process.exit(1);
|
|
4637
|
+
}
|
|
4638
|
+
const prompt = agent.agent_config?.systemPrompt;
|
|
4639
|
+
if (!prompt) {
|
|
4640
|
+
console.error(chalk.red(`Agent "${agent.display_name}" has no systemPrompt`));
|
|
4641
|
+
process.exit(1);
|
|
4642
|
+
}
|
|
4643
|
+
const content = buildAgentFileContent(agent, !!opts.includeFrontmatter);
|
|
4644
|
+
writeFileSync(resolve(file), content, 'utf-8');
|
|
4645
|
+
console.log(chalk.green(`Exported ${agent.display_name} to ${resolve(file)} (${prompt.length} chars)`));
|
|
4646
|
+
}
|
|
4647
|
+
catch (err) {
|
|
4648
|
+
console.error(chalk.red(err.message));
|
|
4649
|
+
process.exit(1);
|
|
4650
|
+
}
|
|
4651
|
+
});
|
|
4288
4652
|
// ── Health ──────────────────────────────────────────────────
|
|
4289
4653
|
program
|
|
4290
4654
|
.command('health')
|