@withone/cli 1.54.1 → 1.55.1
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/README.md +5 -0
- package/dist/{chunk-XN4ZWMLX.js → chunk-FS4HAKZ6.js} +21 -1
- package/dist/index.js +229 -150
- package/dist/{migrate-UGDDHWGY.js → migrate-37A463L6.js} +1 -1
- package/package.json +1 -1
- package/profiles/gmail/gmailThreads.json +6 -0
- package/skills/one/SKILL.md +2 -0
- package/skills/one/references/flows.md +6 -0
package/README.md
CHANGED
|
@@ -587,8 +587,13 @@ Resume a paused or failed workflow run from where it left off.
|
|
|
587
587
|
|
|
588
588
|
```bash
|
|
589
589
|
one flow resume abc123
|
|
590
|
+
one flow resume abc123 --allow-bash # required if the flow has bash steps
|
|
590
591
|
```
|
|
591
592
|
|
|
593
|
+
`--allow-bash` is gated the same way as on `flow execute` — resuming doesn't
|
|
594
|
+
inherit the permission from the original run, so pass it again. `one flow list`
|
|
595
|
+
shows which flows need it.
|
|
596
|
+
|
|
592
597
|
### `one flow runs [flowKey]`
|
|
593
598
|
|
|
594
599
|
List workflow runs, optionally filtered by workflow key.
|
|
@@ -523,7 +523,9 @@ async function writePageToMemory(profile, records, opts = {}) {
|
|
|
523
523
|
const searchablePaths = getSearchablePaths(profile);
|
|
524
524
|
const declaresIdentityKeys = (profile.identityKeys?.length ?? 0) > 0;
|
|
525
525
|
const enrichTimestampField = profile.enrich?.timestampField ?? "_enriched_at";
|
|
526
|
-
for (const
|
|
526
|
+
for (const rawRecord of records) {
|
|
527
|
+
const derived = deriveFields(rawRecord, profile.derive);
|
|
528
|
+
const record = Object.keys(derived).length > 0 ? { ...rawRecord, ...derived } : rawRecord;
|
|
527
529
|
report.attempted++;
|
|
528
530
|
const externalId = getByDotPath(record, profile.idField);
|
|
529
531
|
if (externalId === void 0 || externalId === null || externalId === "") {
|
|
@@ -608,6 +610,24 @@ function identityValuesFor(prefix, raw, opts = {}) {
|
|
|
608
610
|
const v = s.toLowerCase().trim();
|
|
609
611
|
return v ? [v] : [];
|
|
610
612
|
}
|
|
613
|
+
function deriveFields(record, derive) {
|
|
614
|
+
const out = {};
|
|
615
|
+
if (!derive) return out;
|
|
616
|
+
for (const [field, spec] of Object.entries(derive)) {
|
|
617
|
+
const { path: path5, extract } = typeof spec === "string" ? { path: spec, extract: void 0 } : spec;
|
|
618
|
+
if (!path5) continue;
|
|
619
|
+
const values = resolveIdentityPath(record, path5).filter((v) => v !== null && v !== void 0 && typeof v !== "object");
|
|
620
|
+
if (values.length === 0) continue;
|
|
621
|
+
if (extract === "email") {
|
|
622
|
+
const emails = values.flatMap((v) => identityValuesFor("email", v));
|
|
623
|
+
if (emails.length === 0) continue;
|
|
624
|
+
out[field] = emails[0];
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
out[field] = values[0];
|
|
628
|
+
}
|
|
629
|
+
return out;
|
|
630
|
+
}
|
|
611
631
|
function tokenizeIdentityPath(path5) {
|
|
612
632
|
const tokens = [];
|
|
613
633
|
const re = /([^.[\]]+)|\[([^\]]*)\]/g;
|
package/dist/index.js
CHANGED
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
writeDraftProfile,
|
|
67
67
|
writePageToMemory,
|
|
68
68
|
writeProfile
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-FS4HAKZ6.js";
|
|
70
70
|
import {
|
|
71
71
|
getByDotPath
|
|
72
72
|
} from "./chunk-44CV5IMX.js";
|
|
@@ -150,15 +150,15 @@ import {
|
|
|
150
150
|
|
|
151
151
|
// src/cli.ts
|
|
152
152
|
import { createRequire as createRequire3 } from "module";
|
|
153
|
-
import
|
|
153
|
+
import path12 from "path";
|
|
154
154
|
import { Command } from "commander";
|
|
155
155
|
|
|
156
156
|
// src/commands/init.ts
|
|
157
157
|
import * as p3 from "@clack/prompts";
|
|
158
158
|
import pc2 from "picocolors";
|
|
159
159
|
import fs3 from "fs";
|
|
160
|
-
import
|
|
161
|
-
import { fileURLToPath as
|
|
160
|
+
import path4 from "path";
|
|
161
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
162
162
|
|
|
163
163
|
// src/lib/agents.ts
|
|
164
164
|
import fs from "fs";
|
|
@@ -649,16 +649,42 @@ import open2 from "open";
|
|
|
649
649
|
|
|
650
650
|
// src/lib/skill-sync.ts
|
|
651
651
|
import fs2 from "fs";
|
|
652
|
-
import
|
|
653
|
-
import { fileURLToPath } from "url";
|
|
652
|
+
import path3 from "path";
|
|
653
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
654
654
|
|
|
655
655
|
// src/commands/update.ts
|
|
656
|
-
import { createRequire } from "module";
|
|
657
656
|
import { spawn } from "child_process";
|
|
658
657
|
import { readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
659
658
|
import { join } from "path";
|
|
659
|
+
|
|
660
|
+
// src/lib/version.ts
|
|
661
|
+
import { createRequire } from "module";
|
|
662
|
+
import { existsSync } from "fs";
|
|
663
|
+
import path2 from "path";
|
|
664
|
+
import { fileURLToPath } from "url";
|
|
660
665
|
var require2 = createRequire(import.meta.url);
|
|
661
|
-
var
|
|
666
|
+
var cached = null;
|
|
667
|
+
function cliVersion() {
|
|
668
|
+
if (cached !== null) return cached;
|
|
669
|
+
const dir = path2.dirname(fileURLToPath(import.meta.url));
|
|
670
|
+
for (let i = 1; i <= 4; i++) {
|
|
671
|
+
const candidate = path2.resolve(dir, ...Array(i).fill(".."), "package.json");
|
|
672
|
+
if (!existsSync(candidate)) continue;
|
|
673
|
+
try {
|
|
674
|
+
const version2 = require2(candidate).version;
|
|
675
|
+
if (version2) {
|
|
676
|
+
cached = version2;
|
|
677
|
+
return cached;
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
cached = "unknown";
|
|
683
|
+
return cached;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/commands/update.ts
|
|
687
|
+
var currentVersion = cliVersion();
|
|
662
688
|
var ONE_DIR = () => join(homeDir(), ".one");
|
|
663
689
|
var CACHE_PATH = () => join(ONE_DIR(), "update-check.json");
|
|
664
690
|
var LOCK_PATH = () => join(ONE_DIR(), "auto-update.lock");
|
|
@@ -814,17 +840,17 @@ function autoUpdate(targetVersion, publishedAt) {
|
|
|
814
840
|
var CANONICAL_SKILL_DIR = ".agents/skills";
|
|
815
841
|
var VERSION_MARKER = ".one-cli-version";
|
|
816
842
|
function getPackagedSkillDir() {
|
|
817
|
-
const here =
|
|
818
|
-
return
|
|
843
|
+
const here = path3.dirname(fileURLToPath2(import.meta.url));
|
|
844
|
+
return path3.resolve(here, "..", "skills", "one");
|
|
819
845
|
}
|
|
820
846
|
function getCanonicalSkillPath() {
|
|
821
|
-
return
|
|
847
|
+
return path3.join(homeDir(), CANONICAL_SKILL_DIR, "one");
|
|
822
848
|
}
|
|
823
849
|
function getVersionMarkerPath() {
|
|
824
|
-
return
|
|
850
|
+
return path3.join(getCanonicalSkillPath(), VERSION_MARKER);
|
|
825
851
|
}
|
|
826
852
|
function isSkillInstalled() {
|
|
827
|
-
return fs2.existsSync(
|
|
853
|
+
return fs2.existsSync(path3.join(getCanonicalSkillPath(), "SKILL.md"));
|
|
828
854
|
}
|
|
829
855
|
function readInstalledSkillVersion() {
|
|
830
856
|
try {
|
|
@@ -844,8 +870,8 @@ function writeInstalledSkillVersion(version2) {
|
|
|
844
870
|
function copyDirSync(src, dest) {
|
|
845
871
|
fs2.mkdirSync(dest, { recursive: true });
|
|
846
872
|
for (const entry of fs2.readdirSync(src, { withFileTypes: true })) {
|
|
847
|
-
const srcPath =
|
|
848
|
-
const destPath =
|
|
873
|
+
const srcPath = path3.join(src, entry.name);
|
|
874
|
+
const destPath = path3.join(dest, entry.name);
|
|
849
875
|
if (entry.isDirectory()) {
|
|
850
876
|
copyDirSync(srcPath, destPath);
|
|
851
877
|
} else {
|
|
@@ -872,7 +898,7 @@ function forceSyncSkills() {
|
|
|
872
898
|
}
|
|
873
899
|
function performSync(current, reason) {
|
|
874
900
|
const source = getPackagedSkillDir();
|
|
875
|
-
if (!fs2.existsSync(
|
|
901
|
+
if (!fs2.existsSync(path3.join(source, "SKILL.md"))) {
|
|
876
902
|
return { synced: false, reason: "source-missing" };
|
|
877
903
|
}
|
|
878
904
|
const canonical = getCanonicalSkillPath();
|
|
@@ -1240,7 +1266,7 @@ async function chooseConfigScope(options) {
|
|
|
1240
1266
|
const hasGlobal = globalConfigExists();
|
|
1241
1267
|
const hasProject = projectConfigExists();
|
|
1242
1268
|
const projectRoot = resolved.projectRoot;
|
|
1243
|
-
const projectName =
|
|
1269
|
+
const projectName = path4.basename(projectRoot);
|
|
1244
1270
|
const homeGlobal = tildify(getGlobalConfigPath());
|
|
1245
1271
|
const homeProject = tildify(getProjectConfigPath(projectRoot));
|
|
1246
1272
|
if (hasProject) {
|
|
@@ -1525,26 +1551,26 @@ var SKILL_AGENTS = [
|
|
|
1525
1551
|
];
|
|
1526
1552
|
var CANONICAL_SKILL_DIR2 = ".agents/skills";
|
|
1527
1553
|
function getSkillSourceDir() {
|
|
1528
|
-
const __dirname2 =
|
|
1529
|
-
return
|
|
1554
|
+
const __dirname2 = path4.dirname(fileURLToPath3(import.meta.url));
|
|
1555
|
+
return path4.resolve(__dirname2, "..", "skills", "one");
|
|
1530
1556
|
}
|
|
1531
1557
|
function getCanonicalSkillPath2() {
|
|
1532
|
-
return
|
|
1558
|
+
return path4.join(homeDir(), CANONICAL_SKILL_DIR2, "one");
|
|
1533
1559
|
}
|
|
1534
1560
|
function getAgentSkillPath(agent) {
|
|
1535
|
-
return
|
|
1561
|
+
return path4.join(homeDir(), agent.skillDir, "one");
|
|
1536
1562
|
}
|
|
1537
1563
|
function isSkillInstalled2() {
|
|
1538
|
-
return fs3.existsSync(
|
|
1564
|
+
return fs3.existsSync(path4.join(getCanonicalSkillPath2(), "SKILL.md"));
|
|
1539
1565
|
}
|
|
1540
1566
|
function isSkillInstalledForAgent(agent) {
|
|
1541
|
-
return fs3.existsSync(
|
|
1567
|
+
return fs3.existsSync(path4.join(getAgentSkillPath(agent), "SKILL.md"));
|
|
1542
1568
|
}
|
|
1543
1569
|
function copyDirSync2(src, dest) {
|
|
1544
1570
|
fs3.mkdirSync(dest, { recursive: true });
|
|
1545
1571
|
for (const entry of fs3.readdirSync(src, { withFileTypes: true })) {
|
|
1546
|
-
const srcPath =
|
|
1547
|
-
const destPath =
|
|
1572
|
+
const srcPath = path4.join(src, entry.name);
|
|
1573
|
+
const destPath = path4.join(dest, entry.name);
|
|
1548
1574
|
if (entry.isDirectory()) {
|
|
1549
1575
|
copyDirSync2(srcPath, destPath);
|
|
1550
1576
|
} else {
|
|
@@ -1557,7 +1583,7 @@ function installSkillForAgents(agentIds) {
|
|
|
1557
1583
|
const canonical = getCanonicalSkillPath2();
|
|
1558
1584
|
const installed = [];
|
|
1559
1585
|
const failed = [];
|
|
1560
|
-
if (!fs3.existsSync(
|
|
1586
|
+
if (!fs3.existsSync(path4.join(source, "SKILL.md"))) {
|
|
1561
1587
|
return { installed: [], failed: ["skill source not found"] };
|
|
1562
1588
|
}
|
|
1563
1589
|
try {
|
|
@@ -1579,14 +1605,14 @@ function installSkillForAgents(agentIds) {
|
|
|
1579
1605
|
continue;
|
|
1580
1606
|
}
|
|
1581
1607
|
try {
|
|
1582
|
-
const agentSkillsDir =
|
|
1608
|
+
const agentSkillsDir = path4.dirname(agentPath);
|
|
1583
1609
|
fs3.mkdirSync(agentSkillsDir, { recursive: true });
|
|
1584
1610
|
try {
|
|
1585
1611
|
fs3.lstatSync(agentPath);
|
|
1586
1612
|
fs3.rmSync(agentPath, { recursive: true });
|
|
1587
1613
|
} catch {
|
|
1588
1614
|
}
|
|
1589
|
-
const relative =
|
|
1615
|
+
const relative = path4.relative(agentSkillsDir, canonical);
|
|
1590
1616
|
fs3.symlinkSync(relative, agentPath);
|
|
1591
1617
|
installed.push(agent.name);
|
|
1592
1618
|
seen.set(agentPath, true);
|
|
@@ -1833,7 +1859,7 @@ ${whoami.user.name} ${pc2.dim(`(${whoami.user.email})`)}`,
|
|
|
1833
1859
|
};
|
|
1834
1860
|
await promptConnectIntegrations(apiKey, connParams);
|
|
1835
1861
|
const savedPath = scope === "project" ? getProjectConfigPath() : getGlobalConfigPath();
|
|
1836
|
-
const resolutionHint = scope === "project" ? `When you run ${pc2.cyan("one")} from ${pc2.bold(
|
|
1862
|
+
const resolutionHint = scope === "project" ? `When you run ${pc2.cyan("one")} from ${pc2.bold(path4.basename(getProjectRoot()))}, it uses this project config.
|
|
1837
1863
|
From anywhere else, it falls back to your global config.` : `This config applies to every folder unless a project config is set.`;
|
|
1838
1864
|
p3.note(
|
|
1839
1865
|
`${scopeLabel(scope)} Config saved to:
|
|
@@ -2523,11 +2549,11 @@ async function actionsSearchCommand(platform, query, options) {
|
|
|
2523
2549
|
const useCache = options.cache !== false;
|
|
2524
2550
|
const searchType = agentType || "knowledge";
|
|
2525
2551
|
const cachePath = searchCachePath(platform, query, searchType);
|
|
2526
|
-
const
|
|
2552
|
+
const cached2 = useCache ? readCache(cachePath) : null;
|
|
2527
2553
|
let cleanedActions;
|
|
2528
2554
|
let cacheHit = false;
|
|
2529
|
-
if (
|
|
2530
|
-
cleanedActions =
|
|
2555
|
+
if (cached2 && isFresh(cached2)) {
|
|
2556
|
+
cleanedActions = cached2.data.actions;
|
|
2531
2557
|
cacheHit = true;
|
|
2532
2558
|
} else {
|
|
2533
2559
|
try {
|
|
@@ -2535,12 +2561,12 @@ async function actionsSearchCommand(platform, query, options) {
|
|
|
2535
2561
|
platform,
|
|
2536
2562
|
query,
|
|
2537
2563
|
agentType,
|
|
2538
|
-
|
|
2564
|
+
cached2?.etag ?? void 0
|
|
2539
2565
|
);
|
|
2540
|
-
if (result.status === 304 &&
|
|
2541
|
-
|
|
2542
|
-
writeCache(cachePath,
|
|
2543
|
-
cleanedActions =
|
|
2566
|
+
if (result.status === 304 && cached2) {
|
|
2567
|
+
cached2.cachedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2568
|
+
writeCache(cachePath, cached2);
|
|
2569
|
+
cleanedActions = cached2.data.actions;
|
|
2544
2570
|
cacheHit = true;
|
|
2545
2571
|
} else {
|
|
2546
2572
|
let actions2 = result.data;
|
|
@@ -2559,12 +2585,12 @@ async function actionsSearchCommand(platform, query, options) {
|
|
|
2559
2585
|
));
|
|
2560
2586
|
}
|
|
2561
2587
|
} catch (fetchError) {
|
|
2562
|
-
if (
|
|
2588
|
+
if (cached2) {
|
|
2563
2589
|
process.stderr.write(
|
|
2564
|
-
`Warning: serving cached search results (network unavailable, cached ${formatAge(getAge(
|
|
2590
|
+
`Warning: serving cached search results (network unavailable, cached ${formatAge(getAge(cached2))} ago)
|
|
2565
2591
|
`
|
|
2566
2592
|
);
|
|
2567
|
-
cleanedActions =
|
|
2593
|
+
cleanedActions = cached2.data.actions;
|
|
2568
2594
|
cacheHit = true;
|
|
2569
2595
|
} else {
|
|
2570
2596
|
throw fetchError;
|
|
@@ -2573,8 +2599,8 @@ async function actionsSearchCommand(platform, query, options) {
|
|
|
2573
2599
|
}
|
|
2574
2600
|
if (isAgentMode()) {
|
|
2575
2601
|
const response = { actions: cleanedActions };
|
|
2576
|
-
if (cacheHit &&
|
|
2577
|
-
response._cache = buildCacheMeta(
|
|
2602
|
+
if (cacheHit && cached2) {
|
|
2603
|
+
response._cache = buildCacheMeta(cached2, true);
|
|
2578
2604
|
} else {
|
|
2579
2605
|
const freshEntry = readCache(cachePath);
|
|
2580
2606
|
response._cache = buildCacheMeta(freshEntry, false);
|
|
@@ -3196,7 +3222,7 @@ import pc7 from "picocolors";
|
|
|
3196
3222
|
|
|
3197
3223
|
// src/lib/flow-validator.ts
|
|
3198
3224
|
import fs4 from "fs";
|
|
3199
|
-
import
|
|
3225
|
+
import path5 from "path";
|
|
3200
3226
|
import { spawnSync } from "child_process";
|
|
3201
3227
|
function validateFlowSchema(flow2) {
|
|
3202
3228
|
const errors = [];
|
|
@@ -3271,32 +3297,32 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
3271
3297
|
const validTypes = FLOW_SCHEMA.stepTypes.map((st) => st.type);
|
|
3272
3298
|
for (let i = 0; i < steps.length; i++) {
|
|
3273
3299
|
const step = steps[i];
|
|
3274
|
-
const
|
|
3300
|
+
const path13 = `${pathPrefix}[${i}]`;
|
|
3275
3301
|
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
3276
|
-
errors.push({ path:
|
|
3302
|
+
errors.push({ path: path13, message: "Step must be an object" });
|
|
3277
3303
|
continue;
|
|
3278
3304
|
}
|
|
3279
3305
|
const s = step;
|
|
3280
3306
|
if (!s.id || typeof s.id !== "string") {
|
|
3281
|
-
errors.push({ path: `${
|
|
3307
|
+
errors.push({ path: `${path13}.id`, message: 'Step must have a string "id"' });
|
|
3282
3308
|
}
|
|
3283
3309
|
if (!s.name || typeof s.name !== "string") {
|
|
3284
|
-
errors.push({ path: `${
|
|
3310
|
+
errors.push({ path: `${path13}.name`, message: 'Step must have a string "name"' });
|
|
3285
3311
|
}
|
|
3286
3312
|
if (!s.type || !validTypes.includes(s.type)) {
|
|
3287
|
-
errors.push({ path: `${
|
|
3313
|
+
errors.push({ path: `${path13}.type`, message: `Step type must be one of: ${validTypes.join(", ")}` });
|
|
3288
3314
|
continue;
|
|
3289
3315
|
}
|
|
3290
3316
|
if (s.requires !== void 0) {
|
|
3291
3317
|
if (!Array.isArray(s.requires)) {
|
|
3292
|
-
errors.push({ path: `${
|
|
3318
|
+
errors.push({ path: `${path13}.requires`, message: '"requires" must be an array of selector strings (e.g. ["$.steps.foo.output.bar"])' });
|
|
3293
3319
|
} else {
|
|
3294
3320
|
for (let r = 0; r < s.requires.length; r++) {
|
|
3295
3321
|
const sel = s.requires[r];
|
|
3296
3322
|
if (typeof sel !== "string") {
|
|
3297
|
-
errors.push({ path: `${
|
|
3323
|
+
errors.push({ path: `${path13}.requires[${r}]`, message: '"requires" entry must be a selector string' });
|
|
3298
3324
|
} else if (!sel.startsWith("$.")) {
|
|
3299
|
-
errors.push({ path: `${
|
|
3325
|
+
errors.push({ path: `${path13}.requires[${r}]`, message: `"requires" entry "${sel}" must be a selector starting with "$." (e.g. "$.steps.foo.output.bar")` });
|
|
3300
3326
|
}
|
|
3301
3327
|
}
|
|
3302
3328
|
}
|
|
@@ -3304,7 +3330,7 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
3304
3330
|
if (s.onError && typeof s.onError === "object") {
|
|
3305
3331
|
const oe = s.onError;
|
|
3306
3332
|
if (!FLOW_SCHEMA.errorStrategies.includes(oe.strategy)) {
|
|
3307
|
-
errors.push({ path: `${
|
|
3333
|
+
errors.push({ path: `${path13}.onError.strategy`, message: `Error strategy must be one of: ${FLOW_SCHEMA.errorStrategies.join(", ")}` });
|
|
3308
3334
|
}
|
|
3309
3335
|
}
|
|
3310
3336
|
const descriptor = getStepTypeDescriptor(s.type);
|
|
@@ -3314,14 +3340,14 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
3314
3340
|
if (!configObj || typeof configObj !== "object") {
|
|
3315
3341
|
const hint = detectFlatConfigHint(s, descriptor);
|
|
3316
3342
|
errors.push({
|
|
3317
|
-
path: `${
|
|
3343
|
+
path: `${path13}.${configKey}`,
|
|
3318
3344
|
message: `${capitalize(descriptor.type)} step must have a "${configKey}" config object${hint}`
|
|
3319
3345
|
});
|
|
3320
3346
|
continue;
|
|
3321
3347
|
}
|
|
3322
3348
|
const config2 = configObj;
|
|
3323
3349
|
for (const [fieldName, fd] of Object.entries(descriptor.fields)) {
|
|
3324
|
-
const fieldPath = `${
|
|
3350
|
+
const fieldPath = `${path13}.${configKey}.${fieldName}`;
|
|
3325
3351
|
const value = config2[fieldName];
|
|
3326
3352
|
if (fd.required && (value === void 0 || value === null || value === "")) {
|
|
3327
3353
|
errors.push({ path: fieldPath, message: `${capitalize(descriptor.type)} must have ${fd.type === "string" ? "a string" : fd.type === "array" ? "a" : "a"} "${fieldName}"` });
|
|
@@ -3354,30 +3380,30 @@ function validateStepsArray(steps, pathPrefix, errors) {
|
|
|
3354
3380
|
}
|
|
3355
3381
|
}
|
|
3356
3382
|
if (descriptor.type === "action") {
|
|
3357
|
-
validateConnectionForm(config2, `${
|
|
3383
|
+
validateConnectionForm(config2, `${path13}.${configKey}`, errors);
|
|
3358
3384
|
}
|
|
3359
3385
|
if (descriptor.type === "code") {
|
|
3360
3386
|
const hasSource = typeof config2.source === "string" && config2.source.length > 0;
|
|
3361
3387
|
const hasModule = typeof config2.module === "string" && config2.module.length > 0;
|
|
3362
3388
|
if (!hasSource && !hasModule) {
|
|
3363
|
-
errors.push({ path: `${
|
|
3389
|
+
errors.push({ path: `${path13}.${configKey}`, message: 'Code step must define either "source" (inline JS) or "module" (path to .mjs file)' });
|
|
3364
3390
|
} else if (hasSource && hasModule) {
|
|
3365
|
-
errors.push({ path: `${
|
|
3391
|
+
errors.push({ path: `${path13}.${configKey}`, message: 'Code step cannot define both "source" and "module" \u2014 pick one' });
|
|
3366
3392
|
}
|
|
3367
3393
|
if (hasModule) {
|
|
3368
3394
|
const m = config2.module;
|
|
3369
3395
|
if (m.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(m)) {
|
|
3370
|
-
errors.push({ path: `${
|
|
3396
|
+
errors.push({ path: `${path13}.${configKey}.module`, message: "Code module path must be relative to the flow folder (no absolute paths)" });
|
|
3371
3397
|
} else if (m.split(/[\\/]/).includes("..")) {
|
|
3372
|
-
errors.push({ path: `${
|
|
3398
|
+
errors.push({ path: `${path13}.${configKey}.module`, message: 'Code module path must not escape the flow folder ("..")' });
|
|
3373
3399
|
} else if (!m.endsWith(".mjs")) {
|
|
3374
|
-
errors.push({ path: `${
|
|
3400
|
+
errors.push({ path: `${path13}.${configKey}.module`, message: "Code module must be a .mjs file" });
|
|
3375
3401
|
}
|
|
3376
3402
|
}
|
|
3377
3403
|
if (hasSource) {
|
|
3378
3404
|
const syntaxError = checkCodeSourceSyntax(config2.source);
|
|
3379
3405
|
if (syntaxError) {
|
|
3380
|
-
errors.push({ path: `${
|
|
3406
|
+
errors.push({ path: `${path13}.${configKey}.source`, message: `Syntax error in code step: ${syntaxError}` });
|
|
3381
3407
|
}
|
|
3382
3408
|
}
|
|
3383
3409
|
}
|
|
@@ -3449,16 +3475,16 @@ function validateStepIds(flow2) {
|
|
|
3449
3475
|
function collectIds(steps, pathPrefix) {
|
|
3450
3476
|
for (let i = 0; i < steps.length; i++) {
|
|
3451
3477
|
const step = steps[i];
|
|
3452
|
-
const
|
|
3478
|
+
const path13 = `${pathPrefix}[${i}]`;
|
|
3453
3479
|
if (seen.has(step.id)) {
|
|
3454
|
-
errors.push({ path: `${
|
|
3480
|
+
errors.push({ path: `${path13}.id`, message: `Duplicate step ID: "${step.id}"` });
|
|
3455
3481
|
} else {
|
|
3456
3482
|
seen.add(step.id);
|
|
3457
3483
|
}
|
|
3458
3484
|
for (const { configKey, fieldName } of nestedKeys) {
|
|
3459
3485
|
const config2 = step[configKey];
|
|
3460
3486
|
if (config2 && Array.isArray(config2[fieldName])) {
|
|
3461
|
-
collectIds(config2[fieldName], `${
|
|
3487
|
+
collectIds(config2[fieldName], `${path13}.${configKey}.${fieldName}`);
|
|
3462
3488
|
}
|
|
3463
3489
|
}
|
|
3464
3490
|
}
|
|
@@ -3506,7 +3532,7 @@ function validateSelectorReferences(flow2, rootDir) {
|
|
|
3506
3532
|
}
|
|
3507
3533
|
return selectors;
|
|
3508
3534
|
}
|
|
3509
|
-
function checkSelectors(selectors,
|
|
3535
|
+
function checkSelectors(selectors, path13, precedingStepIds) {
|
|
3510
3536
|
for (const selector of selectors) {
|
|
3511
3537
|
const parts = selector.split(".");
|
|
3512
3538
|
if (parts.length < 3) continue;
|
|
@@ -3514,15 +3540,15 @@ function validateSelectorReferences(flow2, rootDir) {
|
|
|
3514
3540
|
if (root === "input") {
|
|
3515
3541
|
const inputName = parts[2];
|
|
3516
3542
|
if (!inputNames.has(inputName)) {
|
|
3517
|
-
errors.push({ path:
|
|
3543
|
+
errors.push({ path: path13, message: `Selector "${selector}" references undefined input "${inputName}"` });
|
|
3518
3544
|
}
|
|
3519
3545
|
} else if (root === "steps") {
|
|
3520
3546
|
const stepId = parts[2].replace(/[\[\]]/g, "").split(/[\[\]]/)[0];
|
|
3521
3547
|
if (!allStepIds.has(stepId)) {
|
|
3522
|
-
errors.push({ path:
|
|
3548
|
+
errors.push({ path: path13, message: `Selector "${selector}" references undefined step "${stepId}"` });
|
|
3523
3549
|
} else if (precedingStepIds && !precedingStepIds.has(stepId)) {
|
|
3524
3550
|
errors.push({
|
|
3525
|
-
path:
|
|
3551
|
+
path: path13,
|
|
3526
3552
|
message: `Selector "${selector}" references step "${stepId}" which is declared after the current step. Steps execute in declaration order, so this will always resolve to undefined at runtime \u2014 move the dependency earlier in the steps array.`
|
|
3527
3553
|
});
|
|
3528
3554
|
}
|
|
@@ -3530,20 +3556,20 @@ function validateSelectorReferences(flow2, rootDir) {
|
|
|
3530
3556
|
}
|
|
3531
3557
|
}
|
|
3532
3558
|
const EXPRESSION_FIELDS = /* @__PURE__ */ new Set(["condition.expression", "while.condition"]);
|
|
3533
|
-
function checkOperatorsInSelectorField(value,
|
|
3559
|
+
function checkOperatorsInSelectorField(value, path13) {
|
|
3534
3560
|
if (typeof value === "string" && value.startsWith("$.")) {
|
|
3535
3561
|
if (value.includes("||")) {
|
|
3536
|
-
errors.push({ path:
|
|
3562
|
+
errors.push({ path: path13, message: `Selector "${value}" contains unsupported operator "||". Selectors in data fields use dot-path resolution, not JS evaluation. Use the "default" field on the input definition instead, or use a "code" step for complex expressions.` });
|
|
3537
3563
|
} else if (value.includes("&&")) {
|
|
3538
|
-
errors.push({ path:
|
|
3564
|
+
errors.push({ path: path13, message: `Selector "${value}" contains unsupported operator "&&". Selectors in data fields use dot-path resolution, not JS evaluation. Use a "condition" step or "code" step for complex expressions.` });
|
|
3539
3565
|
}
|
|
3540
3566
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
3541
3567
|
for (const [k, v] of Object.entries(value)) {
|
|
3542
|
-
checkOperatorsInSelectorField(v, `${
|
|
3568
|
+
checkOperatorsInSelectorField(v, `${path13}.${k}`);
|
|
3543
3569
|
}
|
|
3544
3570
|
} else if (Array.isArray(value)) {
|
|
3545
3571
|
for (let i = 0; i < value.length; i++) {
|
|
3546
|
-
checkOperatorsInSelectorField(value[i], `${
|
|
3572
|
+
checkOperatorsInSelectorField(value[i], `${path13}[${i}]`);
|
|
3547
3573
|
}
|
|
3548
3574
|
}
|
|
3549
3575
|
}
|
|
@@ -3587,7 +3613,7 @@ function validateSelectorReferences(flow2, rootDir) {
|
|
|
3587
3613
|
}
|
|
3588
3614
|
const modulePath = step.type === "code" ? c.module : void 0;
|
|
3589
3615
|
if (rootDir && typeof modulePath === "string" && modulePath.length > 0) {
|
|
3590
|
-
const abs =
|
|
3616
|
+
const abs = path5.resolve(rootDir, modulePath);
|
|
3591
3617
|
if (fs4.existsSync(abs)) {
|
|
3592
3618
|
try {
|
|
3593
3619
|
const moduleText = fs4.readFileSync(abs, "utf-8");
|
|
@@ -3628,10 +3654,10 @@ var VALID_OUTPUT_SCHEMA_TYPES = /* @__PURE__ */ new Set(["string", "number", "bo
|
|
|
3628
3654
|
function isOutputSchemaObject(v) {
|
|
3629
3655
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3630
3656
|
}
|
|
3631
|
-
function walkOutputSchema(schema,
|
|
3657
|
+
function walkOutputSchema(schema, path13) {
|
|
3632
3658
|
let current = schema;
|
|
3633
|
-
for (let i = 0; i <
|
|
3634
|
-
const seg =
|
|
3659
|
+
for (let i = 0; i < path13.length; i++) {
|
|
3660
|
+
const seg = path13[i];
|
|
3635
3661
|
if (typeof current === "string") {
|
|
3636
3662
|
return current === "unknown" || current === "object" || current === "array" ? "opaque" : "opaque";
|
|
3637
3663
|
}
|
|
@@ -3783,7 +3809,7 @@ function validateCodeModules(flow2, rootDir) {
|
|
|
3783
3809
|
const stepPath = `${pathPrefix}[${i}]`;
|
|
3784
3810
|
if (step.type === "code" && step.code?.module) {
|
|
3785
3811
|
const m = step.code.module;
|
|
3786
|
-
const abs =
|
|
3812
|
+
const abs = path5.resolve(rootDir, m);
|
|
3787
3813
|
if (!fs4.existsSync(abs)) {
|
|
3788
3814
|
errors.push({
|
|
3789
3815
|
path: `${stepPath}.code.module`,
|
|
@@ -3916,9 +3942,9 @@ function validateFileReadSchemas(flow2) {
|
|
|
3916
3942
|
|
|
3917
3943
|
// src/commands/flow.ts
|
|
3918
3944
|
import fs5 from "fs";
|
|
3919
|
-
import
|
|
3945
|
+
import path6 from "path";
|
|
3920
3946
|
async function writeFlowResultFile(filePath, meta, steps) {
|
|
3921
|
-
const abs =
|
|
3947
|
+
const abs = path6.resolve(filePath);
|
|
3922
3948
|
const ws = fs5.createWriteStream(abs);
|
|
3923
3949
|
const done = new Promise((resolve, reject) => {
|
|
3924
3950
|
ws.on("finish", () => resolve());
|
|
@@ -4326,7 +4352,7 @@ async function flowValidateCommand(keyOrPath) {
|
|
|
4326
4352
|
const flowPath = resolveFlowPath(keyOrPath);
|
|
4327
4353
|
const content = fs5.readFileSync(flowPath, "utf-8");
|
|
4328
4354
|
flowData = JSON.parse(content);
|
|
4329
|
-
rootDir =
|
|
4355
|
+
rootDir = path6.dirname(flowPath);
|
|
4330
4356
|
}
|
|
4331
4357
|
} catch (err) {
|
|
4332
4358
|
spinner5.stop("Validation failed");
|
|
@@ -4353,7 +4379,7 @@ async function flowValidateCommand(keyOrPath) {
|
|
|
4353
4379
|
}
|
|
4354
4380
|
note(`Workflow "${flowData.key}" passed all validation checks`, "Valid");
|
|
4355
4381
|
}
|
|
4356
|
-
async function flowResumeCommand(runId) {
|
|
4382
|
+
async function flowResumeCommand(runId, options = {}) {
|
|
4357
4383
|
intro(pc7.bgCyan(pc7.black(" One Workflow ")));
|
|
4358
4384
|
const state = FlowRunner.loadRunState(runId);
|
|
4359
4385
|
if (!state) {
|
|
@@ -4374,6 +4400,14 @@ async function flowResumeCommand(runId) {
|
|
|
4374
4400
|
error(`Could not load workflow "${state.flowKey}": ${err instanceof Error ? err.message : String(err)}`);
|
|
4375
4401
|
return;
|
|
4376
4402
|
}
|
|
4403
|
+
if (!options.allowBash && flowRequiresBash(flow2)) {
|
|
4404
|
+
const msg = `Workflow "${flow2.key}" contains bash steps. Re-run with --allow-bash to permit shell execution.`;
|
|
4405
|
+
if (isAgentMode()) {
|
|
4406
|
+
json({ error: msg, requiresBash: true, flowKey: flow2.key, runId });
|
|
4407
|
+
process.exit(1);
|
|
4408
|
+
}
|
|
4409
|
+
error(msg);
|
|
4410
|
+
}
|
|
4377
4411
|
const runner = FlowRunner.fromRunState(state);
|
|
4378
4412
|
const onEvent = (event) => {
|
|
4379
4413
|
if (isAgentMode()) {
|
|
@@ -4383,7 +4417,11 @@ async function flowResumeCommand(runId) {
|
|
|
4383
4417
|
const spinner5 = createSpinner();
|
|
4384
4418
|
spinner5.start(`Resuming run ${runId} (${state.completedSteps.length} steps already completed)...`);
|
|
4385
4419
|
try {
|
|
4386
|
-
const context = await runner.resume(flow2, api, permissions, actionIds, {
|
|
4420
|
+
const context = await runner.resume(flow2, api, permissions, actionIds, {
|
|
4421
|
+
onEvent,
|
|
4422
|
+
rootDir,
|
|
4423
|
+
allowBash: options.allowBash
|
|
4424
|
+
});
|
|
4387
4425
|
spinner5.stop("Workflow completed");
|
|
4388
4426
|
if (isAgentMode()) {
|
|
4389
4427
|
json({
|
|
@@ -4502,7 +4540,7 @@ async function flowInspectCommand(runId, options = {}) {
|
|
|
4502
4540
|
}
|
|
4503
4541
|
console.log();
|
|
4504
4542
|
console.log(` ${pc7.dim(`State: ${statePath}`)}`);
|
|
4505
|
-
console.log(` ${pc7.dim(`Log: ${
|
|
4543
|
+
console.log(` ${pc7.dim(`Log: ${path6.join(".one/flows/.logs", `${state.flowKey}-${state.runId}.log`)}`)}`);
|
|
4506
4544
|
console.log();
|
|
4507
4545
|
}
|
|
4508
4546
|
function colorStatus(status) {
|
|
@@ -5208,10 +5246,10 @@ function handleId(response, config2, records) {
|
|
|
5208
5246
|
|
|
5209
5247
|
// src/lib/memory/sync/state.ts
|
|
5210
5248
|
import fs6 from "fs";
|
|
5211
|
-
import
|
|
5212
|
-
var SYNC_DIR =
|
|
5213
|
-
var STATE_DIR =
|
|
5214
|
-
var LEGACY_SINGLE_FILE =
|
|
5249
|
+
import path7 from "path";
|
|
5250
|
+
var SYNC_DIR = path7.join(".one", "sync");
|
|
5251
|
+
var STATE_DIR = path7.join(SYNC_DIR, "state");
|
|
5252
|
+
var LEGACY_SINGLE_FILE = path7.join(SYNC_DIR, "sync_state.json");
|
|
5215
5253
|
var legacyMigrationDone = false;
|
|
5216
5254
|
function rowToState(row) {
|
|
5217
5255
|
return {
|
|
@@ -5264,7 +5302,7 @@ async function migrateLegacyOnce() {
|
|
|
5264
5302
|
const backend = await getBackend();
|
|
5265
5303
|
const platforms = fs6.readdirSync(STATE_DIR);
|
|
5266
5304
|
for (const platform of platforms) {
|
|
5267
|
-
const platformDir =
|
|
5305
|
+
const platformDir = path7.join(STATE_DIR, platform);
|
|
5268
5306
|
let entries;
|
|
5269
5307
|
try {
|
|
5270
5308
|
entries = fs6.readdirSync(platformDir);
|
|
@@ -5274,7 +5312,7 @@ async function migrateLegacyOnce() {
|
|
|
5274
5312
|
for (const entry of entries) {
|
|
5275
5313
|
if (!entry.endsWith(".json")) continue;
|
|
5276
5314
|
const model = entry.slice(0, -".json".length);
|
|
5277
|
-
const filePath =
|
|
5315
|
+
const filePath = path7.join(platformDir, entry);
|
|
5278
5316
|
try {
|
|
5279
5317
|
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
5280
5318
|
const modelState = JSON.parse(raw);
|
|
@@ -5331,11 +5369,11 @@ async function removeModelState(platform, model) {
|
|
|
5331
5369
|
|
|
5332
5370
|
// src/lib/memory/sync/lock.ts
|
|
5333
5371
|
import fs7 from "fs";
|
|
5334
|
-
import
|
|
5335
|
-
var LOCK_DIR_REL =
|
|
5372
|
+
import path8 from "path";
|
|
5373
|
+
var LOCK_DIR_REL = path8.join(".one", "sync", "locks");
|
|
5336
5374
|
var STALE_MS = 30 * 60 * 1e3;
|
|
5337
5375
|
function lockPath(platform, model) {
|
|
5338
|
-
return
|
|
5376
|
+
return path8.join(LOCK_DIR_REL, `${platform}_${model}`);
|
|
5339
5377
|
}
|
|
5340
5378
|
function isProcessAlive(pid) {
|
|
5341
5379
|
try {
|
|
@@ -5354,7 +5392,7 @@ var SyncLockError = class extends Error {
|
|
|
5354
5392
|
function acquireSyncLock(platform, model) {
|
|
5355
5393
|
fs7.mkdirSync(LOCK_DIR_REL, { recursive: true });
|
|
5356
5394
|
const dir = lockPath(platform, model);
|
|
5357
|
-
const pidFile =
|
|
5395
|
+
const pidFile = path8.join(dir, "pid");
|
|
5358
5396
|
try {
|
|
5359
5397
|
fs7.mkdirSync(dir);
|
|
5360
5398
|
} catch (err) {
|
|
@@ -5412,8 +5450,8 @@ function acquireSyncLock(platform, model) {
|
|
|
5412
5450
|
// src/lib/memory/sync/hooks.ts
|
|
5413
5451
|
import { spawn as spawn2 } from "child_process";
|
|
5414
5452
|
import fs8 from "fs";
|
|
5415
|
-
import
|
|
5416
|
-
var EVENTS_DIR =
|
|
5453
|
+
import path9 from "path";
|
|
5454
|
+
var EVENTS_DIR = path9.join(".one", "sync", "events");
|
|
5417
5455
|
function classifyRecords(db, model, records, idField, tableExists2) {
|
|
5418
5456
|
if (!tableExists2 || records.length === 0) {
|
|
5419
5457
|
return { inserts: records, updates: [] };
|
|
@@ -5460,7 +5498,7 @@ function appendEventLog(events) {
|
|
|
5460
5498
|
if (events.length === 0) return;
|
|
5461
5499
|
const { platform, model } = events[0];
|
|
5462
5500
|
fs8.mkdirSync(EVENTS_DIR, { recursive: true });
|
|
5463
|
-
const logPath =
|
|
5501
|
+
const logPath = path9.join(EVENTS_DIR, `${platform}_${model}.jsonl`);
|
|
5464
5502
|
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
5465
5503
|
fs8.appendFileSync(logPath, lines);
|
|
5466
5504
|
}
|
|
@@ -5566,8 +5604,8 @@ function sleep(ms) {
|
|
|
5566
5604
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5567
5605
|
}
|
|
5568
5606
|
function interpolate(template, record) {
|
|
5569
|
-
return template.replace(/\{?\{(\w+(?:\.\w+)*)\}\}?/g, (_,
|
|
5570
|
-
const parts =
|
|
5607
|
+
return template.replace(/\{?\{(\w+(?:\.\w+)*)\}\}?/g, (_, path13) => {
|
|
5608
|
+
const parts = path13.split(".");
|
|
5571
5609
|
let value = record;
|
|
5572
5610
|
for (const part of parts) {
|
|
5573
5611
|
if (typeof value !== "object" || value === null) return "";
|
|
@@ -5595,8 +5633,8 @@ function deepMerge(target, source) {
|
|
|
5595
5633
|
}
|
|
5596
5634
|
return result;
|
|
5597
5635
|
}
|
|
5598
|
-
function getByDotPath2(obj,
|
|
5599
|
-
const parts =
|
|
5636
|
+
function getByDotPath2(obj, path13) {
|
|
5637
|
+
const parts = path13.split(".");
|
|
5600
5638
|
let current = obj;
|
|
5601
5639
|
for (const part of parts) {
|
|
5602
5640
|
if (current === null || current === void 0 || typeof current !== "object") return void 0;
|
|
@@ -5605,8 +5643,8 @@ function getByDotPath2(obj, path12) {
|
|
|
5605
5643
|
return current;
|
|
5606
5644
|
}
|
|
5607
5645
|
function stripExcludedFields(obj, paths) {
|
|
5608
|
-
for (const
|
|
5609
|
-
stripOnePath(obj,
|
|
5646
|
+
for (const path13 of paths) {
|
|
5647
|
+
stripOnePath(obj, path13.replace(/\[\]/g, ".*").split("."));
|
|
5610
5648
|
}
|
|
5611
5649
|
}
|
|
5612
5650
|
function stripOnePath(obj, parts) {
|
|
@@ -5888,6 +5926,21 @@ async function enrichPhase(api, db, config2, model, idField, connectionKey, plat
|
|
|
5888
5926
|
const duration = elapsed < 1e3 ? `${elapsed}ms` : elapsed < 6e4 ? `${(elapsed / 1e3).toFixed(1)}s` : `${Math.floor(elapsed / 6e4)}m ${Math.floor(elapsed % 6e4 / 1e3)}s`;
|
|
5889
5927
|
return { enriched, skipped, rateLimited, total, duration };
|
|
5890
5928
|
}
|
|
5929
|
+
async function enrichOneForPreview(api, profile, record, connectionKey) {
|
|
5930
|
+
const config2 = profile.enrich;
|
|
5931
|
+
if (!config2) return null;
|
|
5932
|
+
try {
|
|
5933
|
+
const detailAction = (await resolveActionDetails(api, config2.actionId)).details;
|
|
5934
|
+
const detail = await enrichSingleRow(api, detailAction, config2, record, connectionKey, profile.platform);
|
|
5935
|
+
if (!detail) return null;
|
|
5936
|
+
let enrichedData = detail;
|
|
5937
|
+
if (config2.fields && config2.fields.length > 0) enrichedData = pickFields(enrichedData, config2.fields);
|
|
5938
|
+
if (config2.exclude && config2.exclude.length > 0) stripExcludedFields(enrichedData, config2.exclude);
|
|
5939
|
+
return config2.merge !== false ? deepMerge(record, enrichedData) : { ...enrichedData, [profile.idField]: record[profile.idField] };
|
|
5940
|
+
} catch {
|
|
5941
|
+
return null;
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5891
5944
|
async function enrichSingleRow(api, detailAction, config2, row, connectionKey, platform) {
|
|
5892
5945
|
const pathVars = interpolateParams(config2.pathVars, row);
|
|
5893
5946
|
const queryParams = interpolateParams(config2.queryParams, row);
|
|
@@ -6012,8 +6065,8 @@ function sleep2(ms) {
|
|
|
6012
6065
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6013
6066
|
}
|
|
6014
6067
|
function stripFields(record, paths) {
|
|
6015
|
-
for (const
|
|
6016
|
-
stripOnePath2(record,
|
|
6068
|
+
for (const path13 of paths) {
|
|
6069
|
+
stripOnePath2(record, path13.split("."));
|
|
6017
6070
|
}
|
|
6018
6071
|
}
|
|
6019
6072
|
function stripOnePath2(obj, parts) {
|
|
@@ -6879,7 +6932,14 @@ async function testSyncProfile(api, profile) {
|
|
|
6879
6932
|
}));
|
|
6880
6933
|
report.sample = first;
|
|
6881
6934
|
report.samples = records.slice(0, SEARCHABLE_SAMPLE_SIZE);
|
|
6882
|
-
|
|
6935
|
+
if (profile.enrich && (profile.identityKeys?.length ?? 0) > 0 && report.samples.length > 0) {
|
|
6936
|
+
const merged = await enrichOneForPreview(api, profile, report.samples[0], connectionKey);
|
|
6937
|
+
if (merged) {
|
|
6938
|
+
const preview = buildIdentityKeysPreview([merged], profile);
|
|
6939
|
+
if (preview) report.identityKeysPreview = { ...preview, previewedAfterEnrich: true };
|
|
6940
|
+
}
|
|
6941
|
+
}
|
|
6942
|
+
report.identityKeysPreview ??= buildIdentityKeysPreview(report.samples, profile);
|
|
6883
6943
|
report.ok = checks.every((c) => c.ok);
|
|
6884
6944
|
return report;
|
|
6885
6945
|
}
|
|
@@ -7101,13 +7161,13 @@ function inferProfileFromKnowledge(knowledge, modelName, platform) {
|
|
|
7101
7161
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
7102
7162
|
import fs10 from "fs";
|
|
7103
7163
|
import os from "os";
|
|
7104
|
-
import
|
|
7164
|
+
import path11 from "path";
|
|
7105
7165
|
|
|
7106
7166
|
// src/lib/memory/sync/schedule-registry.ts
|
|
7107
7167
|
import fs9 from "fs";
|
|
7108
|
-
import
|
|
7109
|
-
var REGISTRY_DIR = () =>
|
|
7110
|
-
var REGISTRY_FILE = () =>
|
|
7168
|
+
import path10 from "path";
|
|
7169
|
+
var REGISTRY_DIR = () => path10.join(homeDir(), ".one", "sync");
|
|
7170
|
+
var REGISTRY_FILE = () => path10.join(REGISTRY_DIR(), "schedules.json");
|
|
7111
7171
|
function readRaw() {
|
|
7112
7172
|
try {
|
|
7113
7173
|
if (!fs9.existsSync(REGISTRY_FILE())) return { schedules: [] };
|
|
@@ -7126,7 +7186,7 @@ function writeRaw(file) {
|
|
|
7126
7186
|
fs9.renameSync(tmp, REGISTRY_FILE());
|
|
7127
7187
|
}
|
|
7128
7188
|
function makeScheduleId(platform, cwd) {
|
|
7129
|
-
const slug =
|
|
7189
|
+
const slug = path10.basename(cwd).replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
|
|
7130
7190
|
return `${platform}-${slug}`;
|
|
7131
7191
|
}
|
|
7132
7192
|
function listRegistered() {
|
|
@@ -7160,7 +7220,7 @@ function removeRegistered(id) {
|
|
|
7160
7220
|
|
|
7161
7221
|
// src/lib/memory/sync/schedule.ts
|
|
7162
7222
|
var MARKER = "# one-sync";
|
|
7163
|
-
var LOG_DIR_REL =
|
|
7223
|
+
var LOG_DIR_REL = path11.join(".one", "sync", "logs");
|
|
7164
7224
|
function durationToCron(every) {
|
|
7165
7225
|
const match = every.match(/^(\d+)([mhd])$/);
|
|
7166
7226
|
if (!match) return null;
|
|
@@ -7277,7 +7337,7 @@ function migrateLegacyCronEntries() {
|
|
|
7277
7337
|
const modelsMatch = command.match(/--models\s+(\S+)/);
|
|
7278
7338
|
const models = modelsMatch ? modelsMatch[1].split(",") : void 0;
|
|
7279
7339
|
const logMatch = command.match(/>>\s+"([^"]+)"/);
|
|
7280
|
-
const logFile = logMatch ? logMatch[1] :
|
|
7340
|
+
const logFile = logMatch ? logMatch[1] : path11.resolve(cwd, LOG_DIR_REL, `${platform}.log`);
|
|
7281
7341
|
const id = makeScheduleId(platform, cwd);
|
|
7282
7342
|
if (registeredIds.has(id)) continue;
|
|
7283
7343
|
upsertRegistered({
|
|
@@ -7311,9 +7371,9 @@ function addSchedule(opts) {
|
|
|
7311
7371
|
const cwd = process.cwd();
|
|
7312
7372
|
const id = makeScheduleId(opts.platform, cwd);
|
|
7313
7373
|
const replaced = getRegistered(id) !== void 0;
|
|
7314
|
-
const logDir =
|
|
7374
|
+
const logDir = path11.join(cwd, LOG_DIR_REL);
|
|
7315
7375
|
fs10.mkdirSync(logDir, { recursive: true });
|
|
7316
|
-
const logFile =
|
|
7376
|
+
const logFile = path11.join(logDir, `${opts.platform}.log`);
|
|
7317
7377
|
const entry = {
|
|
7318
7378
|
id,
|
|
7319
7379
|
platform: opts.platform,
|
|
@@ -7657,11 +7717,11 @@ function isNoise(s) {
|
|
|
7657
7717
|
if (/^-?\d+(\.\d+)?([eE][-+]?\d+)?$/.test(s)) return true;
|
|
7658
7718
|
return false;
|
|
7659
7719
|
}
|
|
7660
|
-
function recordSample(stats,
|
|
7661
|
-
let s = stats.get(
|
|
7720
|
+
function recordSample(stats, path13, value, jsType, recordIndex, totalSamples) {
|
|
7721
|
+
let s = stats.get(path13);
|
|
7662
7722
|
if (!s) {
|
|
7663
7723
|
s = {
|
|
7664
|
-
path:
|
|
7724
|
+
path: path13,
|
|
7665
7725
|
total: totalSamples,
|
|
7666
7726
|
recordIndices: /* @__PURE__ */ new Set(),
|
|
7667
7727
|
lenSum: 0,
|
|
@@ -7670,7 +7730,7 @@ function recordSample(stats, path12, value, jsType, recordIndex, totalSamples) {
|
|
|
7670
7730
|
primaryType: jsType,
|
|
7671
7731
|
examples: []
|
|
7672
7732
|
};
|
|
7673
|
-
stats.set(
|
|
7733
|
+
stats.set(path13, s);
|
|
7674
7734
|
}
|
|
7675
7735
|
s.recordIndices.add(recordIndex);
|
|
7676
7736
|
s.lenSum += value.length;
|
|
@@ -7679,28 +7739,28 @@ function recordSample(stats, path12, value, jsType, recordIndex, totalSamples) {
|
|
|
7679
7739
|
if (s.primaryType !== jsType) s.primaryType = "mixed";
|
|
7680
7740
|
if (s.examples.length < 3 && value.length < 200) s.examples.push(value);
|
|
7681
7741
|
}
|
|
7682
|
-
function walkRecord(record,
|
|
7742
|
+
function walkRecord(record, path13, stats, recordIndex, totalSamples) {
|
|
7683
7743
|
if (record === null || record === void 0) return;
|
|
7684
7744
|
if (typeof record === "string") {
|
|
7685
7745
|
const trimmed = record.trim();
|
|
7686
|
-
if (trimmed) recordSample(stats,
|
|
7746
|
+
if (trimmed) recordSample(stats, path13, trimmed, "string", recordIndex, totalSamples);
|
|
7687
7747
|
return;
|
|
7688
7748
|
}
|
|
7689
7749
|
if (typeof record === "number" || typeof record === "boolean") {
|
|
7690
7750
|
const str = String(record);
|
|
7691
7751
|
const kind = typeof record === "number" ? "number" : "boolean";
|
|
7692
|
-
if (str) recordSample(stats,
|
|
7752
|
+
if (str) recordSample(stats, path13, str, kind, recordIndex, totalSamples);
|
|
7693
7753
|
return;
|
|
7694
7754
|
}
|
|
7695
7755
|
if (Array.isArray(record)) {
|
|
7696
|
-
const childPath =
|
|
7756
|
+
const childPath = path13 ? `${path13}[]` : "[]";
|
|
7697
7757
|
for (const item of record) walkRecord(item, childPath, stats, recordIndex, totalSamples);
|
|
7698
7758
|
return;
|
|
7699
7759
|
}
|
|
7700
7760
|
if (typeof record === "object") {
|
|
7701
7761
|
for (const [key, value] of Object.entries(record)) {
|
|
7702
7762
|
if (key.startsWith("_")) continue;
|
|
7703
|
-
const childPath =
|
|
7763
|
+
const childPath = path13 ? `${path13}.${key}` : key;
|
|
7704
7764
|
walkRecord(value, childPath, stats, recordIndex, totalSamples);
|
|
7705
7765
|
}
|
|
7706
7766
|
}
|
|
@@ -8135,7 +8195,7 @@ function buildSearchablePreview(profile, samples) {
|
|
|
8135
8195
|
const first = samples[0];
|
|
8136
8196
|
const paths = getSearchablePaths(profile);
|
|
8137
8197
|
if (paths) {
|
|
8138
|
-
const perPathAgg = paths.map((
|
|
8198
|
+
const perPathAgg = paths.map((path13) => ({ path: path13, hits: 0, total: samples.length, sample: "" }));
|
|
8139
8199
|
for (const record of samples) {
|
|
8140
8200
|
const { paths: perPath } = extractSearchableFromPaths(record, paths);
|
|
8141
8201
|
perPath.forEach((p10, i) => {
|
|
@@ -8379,7 +8439,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
|
|
|
8379
8439
|
` detected legacy .one/sync/data/${platform}.db (${dbSize}) \u2014 auto-migrating into memory before sync.
|
|
8380
8440
|
`
|
|
8381
8441
|
);
|
|
8382
|
-
const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-
|
|
8442
|
+
const { memMigrateCommand: memMigrateCommand3 } = await import("./migrate-37A463L6.js");
|
|
8383
8443
|
await memMigrateCommand3({ platform, yes: true });
|
|
8384
8444
|
return;
|
|
8385
8445
|
}
|
|
@@ -8388,7 +8448,7 @@ async function maybeAutoMigrateLegacy(platform, models) {
|
|
|
8388
8448
|
initialValue: true
|
|
8389
8449
|
});
|
|
8390
8450
|
if (p7.isCancel(shouldMigrate) || !shouldMigrate) return;
|
|
8391
|
-
const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-
|
|
8451
|
+
const { memMigrateCommand: memMigrateCommand2 } = await import("./migrate-37A463L6.js");
|
|
8392
8452
|
await memMigrateCommand2({ platform, yes: true });
|
|
8393
8453
|
}
|
|
8394
8454
|
async function syncSuggestSearchableCommand(platformModel, options = {}) {
|
|
@@ -9092,16 +9152,16 @@ function projectEmbeddingApiKey(cfg) {
|
|
|
9092
9152
|
}
|
|
9093
9153
|
function redactSecrets(cfg) {
|
|
9094
9154
|
const copy = JSON.parse(JSON.stringify(cfg));
|
|
9095
|
-
for (const
|
|
9096
|
-
const val = getPath(copy,
|
|
9155
|
+
for (const path13 of SECRET_PATHS) {
|
|
9156
|
+
const val = getPath(copy, path13);
|
|
9097
9157
|
if (typeof val === "string" && val.length > 0) {
|
|
9098
|
-
setPath(copy,
|
|
9158
|
+
setPath(copy, path13, `${val.slice(0, 6)}\u2026(redacted, use --show-secrets)`);
|
|
9099
9159
|
}
|
|
9100
9160
|
}
|
|
9101
9161
|
return copy;
|
|
9102
9162
|
}
|
|
9103
|
-
function getPath(obj,
|
|
9104
|
-
const parts =
|
|
9163
|
+
function getPath(obj, path13) {
|
|
9164
|
+
const parts = path13.split(".");
|
|
9105
9165
|
let cur = obj;
|
|
9106
9166
|
for (const part of parts) {
|
|
9107
9167
|
if (cur == null || typeof cur !== "object") return void 0;
|
|
@@ -9109,8 +9169,8 @@ function getPath(obj, path12) {
|
|
|
9109
9169
|
}
|
|
9110
9170
|
return cur;
|
|
9111
9171
|
}
|
|
9112
|
-
function setPath(obj,
|
|
9113
|
-
const parts =
|
|
9172
|
+
function setPath(obj, path13, value) {
|
|
9173
|
+
const parts = path13.split(".");
|
|
9114
9174
|
let cur = obj;
|
|
9115
9175
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
9116
9176
|
const part = parts[i];
|
|
@@ -9120,8 +9180,8 @@ function setPath(obj, path12, value) {
|
|
|
9120
9180
|
cur[parts[parts.length - 1]] = value;
|
|
9121
9181
|
return obj;
|
|
9122
9182
|
}
|
|
9123
|
-
function unsetPath(obj,
|
|
9124
|
-
const parts =
|
|
9183
|
+
function unsetPath(obj, path13) {
|
|
9184
|
+
const parts = path13.split(".");
|
|
9125
9185
|
let cur = obj;
|
|
9126
9186
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
9127
9187
|
const part = parts[i];
|
|
@@ -10859,6 +10919,30 @@ Enrichment runs after list sync completes (Phase 2), not inline. It's inherently
|
|
|
10859
10919
|
|
|
10860
10920
|
**Limitation:** Each profile supports one enrich action. If you need multiple enrichments (e.g. both summary and transcript from Fathom), create a second profile/model for the second enrichment.
|
|
10861
10921
|
|
|
10922
|
+
## Derived fields (\`derive\`)
|
|
10923
|
+
|
|
10924
|
+
Add flat, queryable top-level fields computed from paths already in the record \u2014 no shell, no \`jq\`:
|
|
10925
|
+
|
|
10926
|
+
\`\`\`json
|
|
10927
|
+
{
|
|
10928
|
+
"derive": {
|
|
10929
|
+
"from_email": {
|
|
10930
|
+
"path": "messages[0].payload.headers[name=From].value",
|
|
10931
|
+
"extract": "email"
|
|
10932
|
+
},
|
|
10933
|
+
"company": "organization.name"
|
|
10934
|
+
}
|
|
10935
|
+
}
|
|
10936
|
+
\`\`\`
|
|
10937
|
+
|
|
10938
|
+
- Paths use the same resolver as \`identityKeys\`, so \`[]\` wildcards, \`[0]\` indexes and \`[name=From]\` filters all work
|
|
10939
|
+
- \`extract: "email"\` pulls the address out of a display-name header (\`"Jane <jane@acme.com>"\` \u2192 \`jane@acme.com\`), lowercased
|
|
10940
|
+
- A path resolving to nothing **omits** the field rather than writing null, so \`--where\` filters behave
|
|
10941
|
+
- A path resolving to several values takes the first \u2014 it's a flat field by definition, and the path syntax lets you be specific
|
|
10942
|
+
- Applied on both sync phases, so an enriching profile gets the same field whether the record came from the list or the detail pass
|
|
10943
|
+
|
|
10944
|
+
**Prefer \`derive\` over \`transform\` for extracting a field.** \`transform\` spawns \`sh -c\`, so it needs \`jq\` (or whatever you invoke) on PATH and does nothing on Windows. \`derive\` is pure and works everywhere \u2014 which is why the built-in \`gmail/gmailThreads\` profile uses it for \`from_email\`. Reach for \`transform\` when you need real computation, not field extraction.
|
|
10945
|
+
|
|
10862
10946
|
## Record Transform
|
|
10863
10947
|
|
|
10864
10948
|
Pipe records through any shell command or flow between fetch and store. The command receives a JSON array on stdin and must return a JSON array on stdout.
|
|
@@ -10933,7 +11017,9 @@ Two ways to tag a record with a cross-platform identifier (e.g. email), dependin
|
|
|
10933
11017
|
]}
|
|
10934
11018
|
\`\`\`
|
|
10935
11019
|
|
|
10936
|
-
Each \`path\` supports \`[]\` wildcards (one key per element) and a \`[name=From]\` equality filter (e.g. Gmail \`messages[].payload.headers[name=From].value\`). \`email\`-prefixed values are email-extracted, so display-name headers (\`"Jane <jane@acme.com>"\`) and comma-lists normalize cleanly. Values are lowercased/trimmed/deduped.
|
|
11020
|
+
Each \`path\` supports \`[]\` wildcards (one key per element) and a \`[name=From]\` equality filter (e.g. Gmail \`messages[].payload.headers[name=From].value\`). \`email\`-prefixed values are email-extracted, so display-name headers (\`"Jane <jane@acme.com>"\`) and comma-lists normalize cleanly. Values are lowercased/trimmed/deduped.
|
|
11021
|
+
|
|
11022
|
+
\`sync test\` previews how many identity keys each record resolves. For an **enriching** profile the participant paths live in the detail payload, so \`sync test\` spends one detail call on the first sample and previews against the merged shape \u2014 you see the keys a real sync would write, not zero plus a promise. If that call can't be made (rate limit, permissions), it falls back to the list-shape preview and says the keys resolve after enrichment.
|
|
10937
11023
|
|
|
10938
11024
|
The built-in \`gmail/gmailThreads\` profile collects From/To/**Cc/Bcc**. Gmail only returns a \`Bcc\` header on messages the authenticated user sent \u2014 it is stripped for recipients \u2014 so Bcc keys appear on your own sent threads and nowhere else.
|
|
10939
11025
|
|
|
@@ -11586,13 +11672,6 @@ function posthogHost() {
|
|
|
11586
11672
|
function posthogKey() {
|
|
11587
11673
|
return process.env.ONE_POSTHOG_KEY || DEFAULT_POSTHOG_KEY;
|
|
11588
11674
|
}
|
|
11589
|
-
function cliVersion() {
|
|
11590
|
-
try {
|
|
11591
|
-
return require3("../package.json").version;
|
|
11592
|
-
} catch {
|
|
11593
|
-
return "unknown";
|
|
11594
|
-
}
|
|
11595
|
-
}
|
|
11596
11675
|
function envName() {
|
|
11597
11676
|
const key = getApiKey();
|
|
11598
11677
|
return key ? getEnvFromApiKey(key) : "live";
|
|
@@ -12065,11 +12144,11 @@ config.command("reset").description("Remove the project config for the current d
|
|
|
12065
12144
|
const configContent = fs13.readFileSync(resolved.path, "utf-8");
|
|
12066
12145
|
fs13.unlinkSync(resolved.path);
|
|
12067
12146
|
const next = resolveConfig();
|
|
12068
|
-
fs13.mkdirSync(
|
|
12147
|
+
fs13.mkdirSync(path12.dirname(resolved.path), { recursive: true });
|
|
12069
12148
|
fs13.writeFileSync(resolved.path, configContent);
|
|
12070
12149
|
let fallbackLabel;
|
|
12071
12150
|
if (next.scope === "project") {
|
|
12072
|
-
fallbackLabel = `parent project config (${
|
|
12151
|
+
fallbackLabel = `parent project config (${path12.basename(next.projectRoot)})`;
|
|
12073
12152
|
} else if (next.scope === "global") {
|
|
12074
12153
|
fallbackLabel = "global config";
|
|
12075
12154
|
} else {
|
|
@@ -12078,7 +12157,7 @@ config.command("reset").description("Remove the project config for the current d
|
|
|
12078
12157
|
if (!isAgentMode()) {
|
|
12079
12158
|
const p10 = await import("@clack/prompts");
|
|
12080
12159
|
const confirmed = await p10.confirm({
|
|
12081
|
-
message: `Delete project config for ${
|
|
12160
|
+
message: `Delete project config for ${path12.basename(resolved.projectRoot)}? Will fall back to ${fallbackLabel}.`,
|
|
12082
12161
|
initialValue: false
|
|
12083
12162
|
});
|
|
12084
12163
|
if (p10.isCancel(confirmed) || !confirmed) {
|
|
@@ -12088,7 +12167,7 @@ config.command("reset").description("Remove the project config for the current d
|
|
|
12088
12167
|
}
|
|
12089
12168
|
fs13.unlinkSync(resolved.path);
|
|
12090
12169
|
try {
|
|
12091
|
-
fs13.rmdirSync(
|
|
12170
|
+
fs13.rmdirSync(path12.dirname(resolved.path));
|
|
12092
12171
|
} catch {
|
|
12093
12172
|
}
|
|
12094
12173
|
if (isAgentMode()) {
|
|
@@ -12152,8 +12231,8 @@ flow.command("list").alias("ls").description("List all workflows in .one/flows/"
|
|
|
12152
12231
|
flow.command("validate <keyOrPath>").description("Validate a workflow JSON file").action(async (keyOrPath) => {
|
|
12153
12232
|
await flowValidateCommand(keyOrPath);
|
|
12154
12233
|
});
|
|
12155
|
-
flow.command("resume <runId>").description("Resume a paused or failed workflow run").action(async (runId) => {
|
|
12156
|
-
await flowResumeCommand(runId);
|
|
12234
|
+
flow.command("resume <runId>").description("Resume a paused or failed workflow run").option("--allow-bash", "Allow bash step execution (disabled by default for security) \u2014 required to resume any flow containing bash steps").action(async (runId, options) => {
|
|
12235
|
+
await flowResumeCommand(runId, options);
|
|
12157
12236
|
});
|
|
12158
12237
|
flow.command("runs [flowKey]").description("List workflow runs (optionally filtered by flow key)").action(async (flowKey) => {
|
|
12159
12238
|
await flowRunsCommand(flowKey);
|
package/package.json
CHANGED
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
{ "prefix": "email", "path": "messages[].payload.headers[name=Cc].value" },
|
|
22
22
|
{ "prefix": "email", "path": "messages[].payload.headers[name=Bcc].value" }
|
|
23
23
|
],
|
|
24
|
+
"derive": {
|
|
25
|
+
"from_email": {
|
|
26
|
+
"path": "messages[0].payload.headers[name=From].value",
|
|
27
|
+
"extract": "email"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
24
30
|
"enrich": {
|
|
25
31
|
"actionId": "conn_mod_def::GJ3ok0Eq0R8::AAzgZVLqTg2iBuITKpJLZg",
|
|
26
32
|
"pathVars": { "userId": "me", "id": "{id}" },
|
package/skills/one/SKILL.md
CHANGED
|
@@ -311,6 +311,8 @@ Without declared paths, the default walker concatenates every string in the reco
|
|
|
311
311
|
|
|
312
312
|
**Connections are late-bound** — profiles use `"connection": { "platform": "<name>" }`, not literal `connectionKey` strings. The key is resolved at sync time, so `one add <platform>` (re-auth) doesn't break the profile. For multi-account platforms, add `"tag": "<connection-tag>"` to disambiguate, and create the tagged connection with `one add <platform> --tag <name>`. Don't hardcode connection keys in profiles.
|
|
313
313
|
|
|
314
|
+
**Extracting a flat field? Use `derive`, not `transform`.** `derive` computes top-level fields from paths already in the record (`"derive": { "from_email": { "path": "messages[0].payload.headers[name=From].value", "extract": "email" } }`), using the same path syntax as `identityKeys`. `transform` spawns `sh -c`, so it needs `jq` on PATH and silently does nothing on Windows — never put one in a profile you intend to share. A path that resolves to nothing omits the field rather than writing null.
|
|
315
|
+
|
|
314
316
|
**Installed profiles do not auto-update.** `sync run` reads only `.one/sync/profiles/<platform>_<model>.json` and never merges the shipped built-in, so a profile created before a capability shipped silently lacks it — a pre-#167 gmail profile writes zero identity keys, forever, with no change in record counts. `sync run` warns when the built-in declares `identityKeys` / `identityKey` / `enrich` / `dateFilter` / `memory` that your copy lacks (agent mode: a `profileDrift` array). Fix with `one sync init <platform> <model>`, which patches rather than overwrites.
|
|
315
317
|
|
|
316
318
|
**Cross-platform identity on a profile.** Two separate fields, and picking the wrong one silently mangles data:
|
|
@@ -686,8 +686,14 @@ one --agent flow execute <key> --allow-bash -i key=value
|
|
|
686
686
|
one --agent flow runs [flowKey]
|
|
687
687
|
one --agent flow inspect <runId> # per-step outputs of a past run (add --full for untruncated)
|
|
688
688
|
one --agent flow resume <runId>
|
|
689
|
+
one --agent flow resume <runId> --allow-bash # required if the flow has bash steps
|
|
689
690
|
```
|
|
690
691
|
|
|
692
|
+
`flow resume` gates bash exactly like `flow execute` — the permission is not
|
|
693
|
+
inherited from the original run. Resuming a bash flow without the flag fails
|
|
694
|
+
fast with `{"error": "...", "requiresBash": true, "flowKey": "...", "runId": "..."}`,
|
|
695
|
+
so re-invoke with `--allow-bash` rather than treating it as a broken run.
|
|
696
|
+
|
|
691
697
|
## Debugging a flow
|
|
692
698
|
|
|
693
699
|
Three tools that turn "re-run the whole 40-step flow to debug step 30" into near-zero-cost inspection:
|