@agentled/cli
Version:
CLI for Agentled — manage workflows, apps, and knowledge from the command line. Zero context-window cost for AI agents.
506 lines (405 loc) • 19.8 kB
JavaScript
/**
* Workspace folder helpers — local agentled_<slug>/ directory used as the
* agent's working tree for building, testing, and iterating on workflows.
*
* Folder layout:
*
* agentled_<slug>/
* ├── README.md # agent-facing guide
* ├── AGENTS.md # Codex/agent guidance for this client workspace
* ├── .agentled/
* │ ├── workspace.json # workspaceId, slug, apiBase, syncedAt
* │ └── cache/
* │ ├── apps.json # app registry
* │ └── models.json # supported models
* ├── docs/
* │ ├── SKILL.md # full skill reference (copied from package)
* │ └── GOTCHAS.md # documented failure modes
* ├── examples/
* │ ├── scaffolds/ # bundled scaffold JSONs (editable)
* │ └── live/ # workflows pulled from the workspace
* ├── fixtures/
* │ └── step-outputs/ # captured step outputs (zero-credit replay)
* ├── tests/ # <wfId>.test.json declarative test files
* ├── tasks/ # local DFE task tracking, not workspace KG
* └── drafts/ # in-progress pipeline JSON files
*/
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, copyFileSync, cpSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
export { resolveWorkspaceMeta, slugifyWorkspaceRef } from './workspace-meta.js';
// ---------------------------------------------------------------------------
// Resolve a workspace folder from cwd: try cwd → walk up → scan children
// ---------------------------------------------------------------------------
export function findWorkspaceDir(cwd = process.cwd()) {
// 1. cwd itself is a workspace
if (existsSync(join(cwd, '.agentled', 'workspace.json')))
return cwd;
// 2. Walk up parents
let current = cwd;
while (true) {
const parent = dirname(current);
if (parent === current)
break;
if (existsSync(join(parent, '.agentled', 'workspace.json')))
return parent;
current = parent;
}
// 3. Scan child dirs for agentled_*
try {
const entries = readdirSync(cwd, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory() || !entry.name.startsWith('agentled_'))
continue;
const wsFile = join(cwd, entry.name, '.agentled', 'workspace.json');
if (existsSync(wsFile))
return join(cwd, entry.name);
}
}
catch {
// cwd unreadable
}
return null;
}
export function readWorkspaceMeta(wsDir) {
const raw = readFileSync(join(wsDir, '.agentled', 'workspace.json'), 'utf-8');
return JSON.parse(raw);
}
export function writeWorkspaceMeta(wsDir, meta) {
writeFileSync(join(wsDir, '.agentled', 'workspace.json'), JSON.stringify(meta, null, 2) + '\n');
}
// ---------------------------------------------------------------------------
// Resolve bundled assets shipped with @agentled/cli
// ---------------------------------------------------------------------------
function pkgRoot() {
const here = fileURLToPath(import.meta.url);
// dist/utils/workspace-folder.js → ../.. → package root
return resolve(dirname(here), '..', '..');
}
export function bundledScaffoldsDir() {
return join(pkgRoot(), 'scaffolds');
}
export function bundledSkillMd() {
return join(pkgRoot(), 'skills', 'agentled', 'SKILL.md');
}
/** Progressive-disclosure reference docs that SKILL.md links to. */
export function bundledSkillReferencesDir() {
return join(pkgRoot(), 'skills', 'agentled', 'references');
}
/** Copy SKILL.md's `references/` folder into a docs dir so the local copy stays a full reference. */
function copySkillReferences(docsDir) {
const refsSrc = bundledSkillReferencesDir();
if (existsSync(refsSrc)) {
cpSync(refsSrc, join(docsDir, 'references'), { recursive: true, force: true });
}
}
// ---------------------------------------------------------------------------
// GOTCHAS.md — embedded so it stays in sync with the CLI release
// ---------------------------------------------------------------------------
export const GOTCHAS_MD = `# Agentled — Documented Failure Modes (GOTCHAS)
These pass JSON syntax validation but silently misbehave at runtime.
Run \`agentled workflows lint <file>\` to catch them statically before deploying.
---
## 1. \`criteria\` not \`conditions\` in entryConditions [CRITERIA_NOT_CONDITIONS]
**Wrong:** \`{ "entryConditions": { "conditions": [...] } }\`
**Correct:** \`{ "entryConditions": { "criteria": [...] } }\`
The executor reads \`entryConditions.criteria\`. The key \`conditions\` is silently ignored.
---
## 2. \`variable\` not \`field\` in criteria items [VARIABLE_NOT_FIELD]
**Wrong:** \`{ "field": "{{steps.x.score}}", "operator": ">", "value": 70 }\`
**Correct:** \`{ "variable": "{{steps.x.score}}", "operator": ">", "value": 70 }\`
Using \`field\` causes the criterion to be silently skipped.
---
## 3. Gmail label_id must be the internal Label_XXXX ID [GMAIL_LABEL_DISPLAY_NAME]
Gmail's API requires \`Label_XXXXXXXXXX\` IDs, not display names.
Add a \`GMAIL_CREATE_LABEL\` step before and use \`{{steps.ensure-label.id}}\`.
---
## 4. \`aiActionWithTools\` with no tools [AI_STEP_TOOLS_REQUIRED]
A step with \`type: "aiActionWithTools"\` must have at least one tool in
\`step.tools\` or \`step.agent.tools\`.
Valid \`builtinType\` values: \`web_search\`, \`file_search\`, \`code_interpreter\`,
\`fetch_website_content\`, \`kg_search\`, \`kg_traverse\`, \`kg_nodes\`, \`kg_write\`,
\`workspace_memory\`.
---
## 5. Email steps need type + approval action + outreachProfile [EMAIL_MISSING_*]
Three required pieces:
- \`pipelineStepPrompt.type: "email"\`
- \`onApproval.action: "schedule-email"\`
- \`outreachProfile\` input page in \`context.inputPages\`
Missing the schedule-email action means the email is drafted but never sent.
---
## 6. Only the first step in a loop gets \`loopConfig\` [LOOP_CONFIG_MULTIPLE_STEPS]
\`loopConfig\` must be on the first step in the loop chain only. Subsequent
steps inside the loop iterate automatically.
---
## 7. Don't pass raw \`{{input.*}}\` directly to search APIs [RAW_INPUT_TO_SEARCH]
Add an aiAction step before the search that generates optimized queries.
Raw user input makes poor search queries.
---
## 8. Child workflows must use \`return\` step, not \`milestone\` [CHILD_WORKFLOW_NO_RETURN]
If a workflow is called via \`agentled.call-workflow\`, use \`type: "return"\`.
\`milestone\` produces no return data. Also set \`context.executionInputConfig.internal: true\`.
---
## 9. Model IDs are internal format, not Anthropic format [MODEL_ID_FORMAT]
Wrong: \`claude-sonnet-4-6\`. Correct: \`claude-4-6-sonnet\`.
Run \`agentled models list\` for valid internal IDs.
---
## 10. Native app actionId must include appId prefix [ACTION_ID_MISSING_PREFIX]
Wrong: \`{ "id": "kg", "actionId": "read-list" }\`
Correct: \`{ "id": "kg", "actionId": "kg.read-list" }\`
---
## 11. \`loop_completion\` criteria requires \`onCriteriaFail: "wait"\` [LOOP_COMPLETION_NOT_WAIT]
Without \`onCriteriaFail: "wait"\`, the step skips instead of blocking until
the loop finishes. The \`stepId\` field is also required.
---
## 12. Arrays in JSON template strings — don't JSON.stringify
The serializer detects when a template variable is the sole content of a JSON
field and inlines the raw value. Pass \`{ "items": "{{steps.x.items}}" }\`
directly — no stringify needed.
---
## 13. \`kg.upsert-rows\` needs \`userKey\` for dedup; \`kg.add-rows\` always inserts
\`kg.upsert-rows\` with \`userKey\`: same key = same row, cross-run dedup O(1).
\`kg.add-rows\`: always inserts a new row, duplicates accumulate.
Use \`mergeStrategy: "merge"\` to preserve downstream-added fields.
`;
// ---------------------------------------------------------------------------
// tasks/README.md — local DFE task guidance for client workspace folders
// ---------------------------------------------------------------------------
export const TASKS_README_MD = `# Tasks
Use this folder for local DFE execution work for this client workspace:
- active tasks and follow-ups
- implementation notes
- blockers and open questions
- done logs and handoff notes
Keep task-level details local. They are operational, may be noisy or provisional, and should not be synced wholesale into workspace knowledge.
Promote only client-facing milestone recaps and alignment memory to workspace \`kg.text\`:
- delivered milestones
- agreed decisions
- important blockers or risks
- next client-facing follow-ups
- context the client or future agent sessions need to stay aligned
Do not store secrets or connector credentials in this folder.
`;
// ---------------------------------------------------------------------------
// AGENTS.md — Codex/agent guidance for client workspace folders
// ---------------------------------------------------------------------------
export function makeAgentsMd(meta) {
return `# AGENTS.md
This folder is a local Agentled client workspace for \`${meta.name}\`.
## Scope
Use this folder to build, test, pull, and iterate on workflows for this workspace. Treat it as local DFE working state, not as the canonical client knowledge base.
## Remote Workspace First
This folder is a working copy. The canonical workspace state lives in Agentled.
Do not treat an empty local folder as an empty workspace.
Before summarizing the client, planning workflow edits, or deciding what exists,
inspect the remote workspace with Agentled MCP tools or the CLI:
- \`get_workspace\`
- \`get_workspace_company_profile\`
- \`list_workflows\`
- \`list_knowledge_lists\`
- \`list_connections\` when available
- \`list_agents\` when available
If MCP tools are not available in the current thread, say that the live tool
registry may be stale and ask the user to restart Codex or open a fresh thread.
Do not substitute a filesystem-only scan for remote workspace orientation.
After remote orientation, compare it to local state:
- pull live workflows into \`examples/live/\`
- start edits in \`drafts/\`
- capture fixtures in \`fixtures/step-outputs/\`
- add declarative tests in \`tests/\`
## Task And Client Alignment Memory
Manage task-level DFE work locally in \`tasks/\`. Do not sync granular todos, scratch notes, implementation task tracking, or in-flight execution details into workspace knowledge.
Use workspace \`kg.text\` only for client-facing milestone recaps and alignment memory:
- delivered milestones
- agreed decisions
- important blockers or risks
- next client-facing follow-ups
- context the client or future agent sessions need to stay aligned
At the end of each meaningful working session, include a concise user-facing retrospective:
- what worked
- what did not work or blocked progress
- concrete improvements for the next session
- follow-up risks
If the feedback is about Agentled itself — CLI behavior, MCP tools, workflow validation, app integrations, docs, missing capabilities, confusing errors, or platform behavior — send it to the Agentled team with the \`submit_feedback_to_agentled\` MCP tool when available. From the CLI, use \`agentled feedback submit --type <bug|feature_request|escalation|ask> --title "<title>" --description "<details>"\`.
## Connectors And Plugins
Email, calendar, Drive, CRM, and other external integrations are configured by the user in their Codex or Agentled environment. Do not store connector credentials, OAuth tokens, or API keys in this folder.
When a workflow requires a connector, document the required connection and ask the user to connect it through the appropriate Codex or Agentled plugin/app.
## Local Workflow Loop
- Pull live workflows into \`examples/live/\`.
- Draft new pipeline JSON in \`drafts/\`.
- Capture fixtures into \`fixtures/step-outputs/\`.
- Add declarative tests in \`tests/\`.
- Use \`agentled workflows lint <file>\` before validating or pushing workflow changes.
`;
}
// ---------------------------------------------------------------------------
// README.md — agent-facing guide written into each new workspace folder
// ---------------------------------------------------------------------------
export function makeWorkspaceReadme(meta) {
return `# Agentled Workspace — ${meta.name}
**Workspace ID:** \`${meta.workspaceId}\`
**Slug:** \`${meta.slug}\`
**API Base:** ${meta.apiBase}
**Last synced:** ${meta.syncedAt}
---
## What this folder is
Local working tree for building, testing, and iterating on workflows for the
\`${meta.slug}\` workspace.
- **\`docs/\`** — SKILL.md (full reference) + GOTCHAS.md (13 failure modes)
- **\`examples/scaffolds/\`** — preflight-clean pipeline JSON skeletons
- **\`examples/live/\`** — pulled copies of real workspace workflows
- **\`fixtures/step-outputs/\`** — captured step outputs for credit-free testing
- **\`tests/\`** — declarative test files for each workflow
- **\`tasks/\`** — local DFE task tracking; only promote milestone recaps to \`kg.text\`
- **\`drafts/\`** — in-progress pipelines before pushing to workspace
- **\`clients/\`** — local DFE client dossiers with contacts, meetings, feedback, decisions, replies, and validation notes
---
## Quick commands
These commands are part of the supported local workflow loop in
\`@agentled/cli >= 0.7.2\`. Run \`agentled --version\`, \`agentled --help\`,
or \`agentled <group> --help\` if a local installation appears out of date.
\`\`\`bash
# Pull a workflow from the workspace to iterate locally
agentled workflows pull <workflowId>
# Static gotcha checks (no API)
agentled workflows lint <file.json>
# Capture step outputs from a past execution
agentled fixture capture <executionId> --wf <workflowId>
# Run declarative tests against captured fixtures (zero credits)
agentled test <workflowId>
# Run tests with live API calls (costs credits)
agentled test <workflowId> --live
# Push a local pipeline to the workspace
agentled workflows create --file <file.json>
# Refresh cached app registry + models
agentled workspace sync
\`\`\`
---
## DFE client dossier loop
Use \`clients/\` to keep a local operating trail for each client engagement.
This stays offline by default and gives Codex, Claude, or another coding agent
the context it needs before changing workflows or drafting client replies.
\`\`\`bash
# Create a local dossier under clients/<client-slug>/
agentled clients init <client-slug> --name "Client Name"
# Capture a meeting note from the standard template
agentled clients meeting new <client-slug> --title "Weekly workflow review"
# List dossiers and check current client status
agentled clients list
agentled clients status <client-slug>
# Emit a compact context packet for an AI agent prompt
agentled clients brief <client-slug>
\`\`\`
Keep credentials and connector secrets out of client dossiers. Store durable
client-facing milestone recaps in workspace knowledge only when they are useful
outside the local sprint trail.
---
## Testing strategy (zero-credit loop)
1. Build the workflow incrementally via MCP tools or CLI.
2. Run once via \`workflows start\` with sample input.
3. Capture outputs: \`agentled fixture capture <execId> --wf <wfId>\`
4. Iterate on step logic, test against fixtures: \`agentled test <wfId>\`
5. For AI step changes, re-test with \`--live\` (costs credits for that step only).
6. Use \`agentled workflows lint\` before every \`workflows validate\` to catch gotchas first.
---
## Folder layout
\`\`\`
agentled_${meta.slug}/
├── README.md
├── AGENTS.md
├── .agentled/
│ ├── workspace.json
│ └── cache/
│ ├── apps.json
│ └── models.json
├── docs/
│ ├── SKILL.md
│ └── GOTCHAS.md
├── examples/
│ ├── scaffolds/
│ └── live/
├── fixtures/
│ └── step-outputs/
├── tests/
├── tasks/
│ └── README.md
├── drafts/
└── clients/
└── <client-slug>/
├── client.json
├── contacts.json
├── workflows.json
├── meetings/
├── decisions/
├── feedback/
├── replies/
└── validation/
\`\`\`
`;
}
// ---------------------------------------------------------------------------
// Folder bootstrap: create dirs, copy bundled assets, write GOTCHAS+README
// ---------------------------------------------------------------------------
export function createWorkspaceFolder(folderPath, meta, opts = {}) {
const dirs = [
folderPath,
join(folderPath, '.agentled', 'cache'),
join(folderPath, 'docs'),
join(folderPath, 'examples', 'scaffolds'),
join(folderPath, 'examples', 'live'),
join(folderPath, 'fixtures', 'step-outputs'),
join(folderPath, 'tests'),
join(folderPath, 'tasks'),
join(folderPath, 'drafts'),
join(folderPath, 'clients'),
];
for (const dir of dirs)
mkdirSync(dir, { recursive: true });
writeWorkspaceMeta(folderPath, meta);
if (opts.appsList) {
writeFileSync(join(folderPath, '.agentled', 'cache', 'apps.json'), JSON.stringify(opts.appsList, null, 2) + '\n');
}
if (opts.modelsList) {
writeFileSync(join(folderPath, '.agentled', 'cache', 'models.json'), JSON.stringify(opts.modelsList, null, 2) + '\n');
}
// Copy SKILL.md (+ its references/) from bundled package
const skillSrc = bundledSkillMd();
if (existsSync(skillSrc)) {
copyFileSync(skillSrc, join(folderPath, 'docs', 'SKILL.md'));
copySkillReferences(join(folderPath, 'docs'));
}
else {
writeFileSync(join(folderPath, 'docs', 'SKILL.md'), '# SKILL.md\n\nRun `agentled workspace sync` to refresh.\n');
}
writeFileSync(join(folderPath, 'docs', 'GOTCHAS.md'), GOTCHAS_MD);
// Copy bundled scaffolds into examples/scaffolds/
const scaffoldDir = bundledScaffoldsDir();
if (existsSync(scaffoldDir)) {
for (const file of readdirSync(scaffoldDir).filter((f) => f.endsWith('.json'))) {
copyFileSync(join(scaffoldDir, file), join(folderPath, 'examples', 'scaffolds', file));
}
}
writeFileSync(join(folderPath, 'README.md'), makeWorkspaceReadme(meta));
writeFileSync(join(folderPath, 'AGENTS.md'), makeAgentsMd(meta));
writeFileSync(join(folderPath, 'tasks', 'README.md'), TASKS_README_MD);
writeFileSync(join(folderPath, 'clients', '.gitkeep'), '');
writeFileSync(join(folderPath, '.gitignore'), ['.agentled/cache/', 'fixtures/', 'tests/_runs/'].join('\n') + '\n');
}
export function refreshWorkspaceFolder(wsDir, opts) {
const cacheDir = join(wsDir, '.agentled', 'cache');
mkdirSync(cacheDir, { recursive: true });
if (opts.appsList)
writeFileSync(join(cacheDir, 'apps.json'), JSON.stringify(opts.appsList, null, 2) + '\n');
if (opts.modelsList)
writeFileSync(join(cacheDir, 'models.json'), JSON.stringify(opts.modelsList, null, 2) + '\n');
const skillSrc = bundledSkillMd();
if (existsSync(skillSrc)) {
copyFileSync(skillSrc, join(wsDir, 'docs', 'SKILL.md'));
copySkillReferences(join(wsDir, 'docs'));
}
writeFileSync(join(wsDir, 'docs', 'GOTCHAS.md'), GOTCHAS_MD);
const meta = readWorkspaceMeta(wsDir);
meta.syncedAt = new Date().toISOString();
writeWorkspaceMeta(wsDir, meta);
}
//# sourceMappingURL=workspace-folder.js.map