diff --git a/extensions/oc-path/src/oc-path/find.ts b/extensions/oc-path/src/oc-path/find.ts index 18d66dfd0f28..10c3b1b11ce2 100644 --- a/extensions/oc-path/src/oc-path/find.ts +++ b/extensions/oc-path/src/oc-path/find.ts @@ -11,6 +11,7 @@ import { expectDefined } from "openclaw/plugin-sdk/expect-runtime"; import { isMap, isScalar, isSeq, type Node, type Pair } from "yaml"; import type { MdAst } from "./ast.js"; import type { JsoncValue } from "./jsonc/ast.js"; +import { resolveJsoncPositionalSegment } from "./jsonc/resolve-value.js"; import type { JsonlAst, JsonlLine } from "./jsonl/ast.js"; import { pickJsonlLine } from "./jsonl/line.js"; import type { OcPath, PredicateSpec } from "./oc-path.js"; @@ -303,11 +304,15 @@ const jsoncOps: WalkOps = { return null; }, positional(node, seg) { - const concrete = positionalForJsoncNode(node, seg); + const concrete = resolveJsoncPositionalSegment(node, seg); if (concrete === null) { return null; } - return jsoncOps.lookup(node, concrete); + const match = jsoncOps.lookup(node, concrete); + if (match === null || node.kind !== "object") { + return match; + } + return { keySub: quoteSeg(concrete), child: match.child }; }, *predicate(node, pred) { if (node.kind === "object") { @@ -327,17 +332,6 @@ const jsoncOps: WalkOps = { walk: walkJsonc, }; -function positionalForJsoncNode(node: JsoncValue, seg: string): string | null { - if (node.kind === "object") { - const keys = node.entries.map((e) => e.key); - return resolvePositionalSeg(seg, { indexable: false, size: keys.length, keys }); - } - if (node.kind === "array") { - return resolvePositionalSeg(seg, { indexable: true, size: node.items.length }); - } - return null; -} - // ---------- JSONL walker --------------------------------------------------- // First slot is a line address; subsequent slots descend into the diff --git a/extensions/oc-path/src/oc-path/jsonc/edit.ts b/extensions/oc-path/src/oc-path/jsonc/edit.ts index c453f5abe4a3..ddb401fb1f86 100644 --- a/extensions/oc-path/src/oc-path/jsonc/edit.ts +++ b/extensions/oc-path/src/oc-path/jsonc/edit.ts @@ -5,13 +5,13 @@ import { isPositionalSeg, isQuotedSeg, parseArrayIndexSegment, - resolvePositionalSeg, splitRespectingBrackets, unquoteSeg, } from "../oc-path.js"; import { OcEmitSentinelError, REDACTED_SENTINEL } from "../sentinel.js"; import type { JsoncAst, JsoncValue } from "./ast.js"; import { parseJsonc } from "./parse.js"; +import { resolveJsoncPositionalSegment } from "./resolve-value.js"; type JsoncEditPath = Array; type JsoncEditTarget = { readonly path: JsoncEditPath; readonly value: JsoncValue }; @@ -126,7 +126,7 @@ function resolveEditTarget(root: JsoncValue, segments: readonly string[]): Jsonc return null; } if (isPositionalSeg(segment)) { - const concrete = positionalForJsonc(current, segment); + const concrete = resolveJsoncPositionalSegment(current, segment); if (concrete !== null) { segment = concrete; } @@ -154,17 +154,6 @@ function resolveEditTarget(root: JsoncValue, segments: readonly string[]): Jsonc return { path: out, value: current }; } -function positionalForJsonc(node: JsoncValue, segment: string): string | null { - if (node.kind === "object") { - const keys = node.entries.map((entry) => entry.key); - return resolvePositionalSeg(segment, { indexable: false, size: keys.length, keys }); - } - if (node.kind === "array") { - return resolvePositionalSeg(segment, { indexable: true, size: node.items.length }); - } - return null; -} - function jsoncValueToJson(value: JsoncValue): unknown { switch (value.kind) { case "object": diff --git a/extensions/oc-path/src/oc-path/jsonc/resolve-value.ts b/extensions/oc-path/src/oc-path/jsonc/resolve-value.ts index 2426abf5cac8..fdb3940cb695 100644 --- a/extensions/oc-path/src/oc-path/jsonc/resolve-value.ts +++ b/extensions/oc-path/src/oc-path/jsonc/resolve-value.ts @@ -23,7 +23,7 @@ export function resolveJsoncValueOcPath( return null; } if (isPositionalSeg(seg)) { - const concrete = positionalForJsonc(current, seg); + const concrete = resolveJsoncPositionalSegment(current, seg); if (concrete !== null) { seg = concrete; } @@ -60,7 +60,7 @@ export function resolveJsoncValueOcPath( return { kind: "value", node: current, path: walked }; } -function positionalForJsonc(node: JsoncValue, seg: string): string | null { +export function resolveJsoncPositionalSegment(node: JsoncValue, seg: string): string | null { if (node.kind === "object") { const keys = node.entries.map((e) => e.key); return resolvePositionalSeg(seg, { indexable: false, size: keys.length, keys }); diff --git a/extensions/oc-path/src/oc-path/tests/find.test.ts b/extensions/oc-path/src/oc-path/tests/find.test.ts index 6948915093e4..cc3e58370f8a 100644 --- a/extensions/oc-path/src/oc-path/tests/find.test.ts +++ b/extensions/oc-path/src/oc-path/tests/find.test.ts @@ -117,6 +117,31 @@ describe("findOcPaths — JSONC kind", () => { } } }); + + it.each([ + { + name: "quotes positional object keys", + raw: '{"items":{"zeta.key":10,"alpha":20}}', + pattern: "oc://config/items/$first", + expectedPath: 'oc://config/items/"zeta.key"', + expectedValue: "10", + }, + { + name: "emits positional array indexes", + raw: '{"items":[10,20,30]}', + pattern: "oc://config/items/$last", + expectedPath: "oc://config/items/2", + expectedValue: "30", + }, + ])("$name", ({ raw, pattern, expectedPath, expectedValue }) => { + const ast = parseJsonc(raw).ast; + const out = findOcPaths(ast, parseOcPath(pattern)); + + expect(out).toHaveLength(1); + const result = requireFirstResult(out); + expect(formatOcPath(result.path)).toBe(expectedPath); + expect(result.match.kind === "leaf" && result.match.valueText).toBe(expectedValue); + }); }); describe("findOcPaths — slash-deep JSONC paths", () => {