@lotics/cli 0.100.2 → 0.101.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/dist/render_page.js +94 -20
- package/dist/src/cli.js +165 -73
- package/dist/src/client.d.ts +9 -4
- package/dist/src/client.js +1 -0
- package/docs/cli_reference.md +2 -2
- package/package.json +1 -1
package/dist/render_page.js
CHANGED
|
@@ -28695,6 +28695,73 @@ ${e.toString()}`);
|
|
|
28695
28695
|
offsetYEmu
|
|
28696
28696
|
};
|
|
28697
28697
|
}
|
|
28698
|
+
var EMU_PER_PT = 12700;
|
|
28699
|
+
var VML_UNIT_TO_EMU = {
|
|
28700
|
+
pt: EMU_PER_PT,
|
|
28701
|
+
in: EMU_PER_PT * 72,
|
|
28702
|
+
cm: EMU_PER_PT * 72 / 2.54,
|
|
28703
|
+
mm: EMU_PER_PT * 72 / 25.4,
|
|
28704
|
+
pc: EMU_PER_PT * 12,
|
|
28705
|
+
px: EMU_PER_PT * 72 / 96
|
|
28706
|
+
};
|
|
28707
|
+
function readVmlLength(style, property) {
|
|
28708
|
+
const match = new RegExp(`(?:^|;)\\s*${property}\\s*:\\s*([0-9.]+)([a-z]*)`, "i").exec(style);
|
|
28709
|
+
if (!match) return null;
|
|
28710
|
+
const value = Number.parseFloat(match[1]);
|
|
28711
|
+
if (!Number.isFinite(value)) return null;
|
|
28712
|
+
const unit = match[2].toLowerCase();
|
|
28713
|
+
const perUnit = unit === "" ? EMU_PER_PT : VML_UNIT_TO_EMU[unit];
|
|
28714
|
+
if (perUnit === void 0) return null;
|
|
28715
|
+
return Math.round(value * perUnit);
|
|
28716
|
+
}
|
|
28717
|
+
function parsePict(pictXml) {
|
|
28718
|
+
if (getTagName(pictXml) !== "w:pict") return null;
|
|
28719
|
+
const imagedata = findFirstByTag(pictXml, "v:imagedata");
|
|
28720
|
+
if (!imagedata) return null;
|
|
28721
|
+
const relationshipId = getAttr(imagedata, "r:id") ?? getAttr(imagedata, "r:embed") ?? null;
|
|
28722
|
+
if (relationshipId === null) return null;
|
|
28723
|
+
const shape = findFirstByTag(pictXml, "v:shape") ?? findFirstByTag(pictXml, "v:rect");
|
|
28724
|
+
const style = shape ? getAttr(shape, "style") ?? "" : "";
|
|
28725
|
+
return {
|
|
28726
|
+
relationshipId,
|
|
28727
|
+
widthEmu: readVmlLength(style, "width"),
|
|
28728
|
+
heightEmu: readVmlLength(style, "height"),
|
|
28729
|
+
alt: (shape ? getAttr(shape, "alt") : void 0) ?? "",
|
|
28730
|
+
// VML positioning is a CSS-ish `style`, not the wp: anchor vocabulary the
|
|
28731
|
+
// anchor fields describe. Reporting it as inline keeps the renderer from
|
|
28732
|
+
// acting on offsets that were never read.
|
|
28733
|
+
inline: true,
|
|
28734
|
+
wrap: null,
|
|
28735
|
+
floatSide: null,
|
|
28736
|
+
behindDoc: false,
|
|
28737
|
+
offsetXEmu: null,
|
|
28738
|
+
offsetYEmu: null
|
|
28739
|
+
};
|
|
28740
|
+
}
|
|
28741
|
+
function parseAlternateContent(el) {
|
|
28742
|
+
if (getTagName(el) !== "mc:AlternateContent") return null;
|
|
28743
|
+
const branches = getChildren(el).filter((child) => {
|
|
28744
|
+
const tag = getTagName(child);
|
|
28745
|
+
return tag === "mc:Choice" || tag === "mc:Fallback";
|
|
28746
|
+
});
|
|
28747
|
+
const ordered = [
|
|
28748
|
+
...branches.filter((b) => getTagName(b) === "mc:Choice"),
|
|
28749
|
+
...branches.filter((b) => getTagName(b) === "mc:Fallback")
|
|
28750
|
+
];
|
|
28751
|
+
for (const branch of ordered) {
|
|
28752
|
+
const drawing = findFirstByTag(branch, "w:drawing");
|
|
28753
|
+
if (drawing) {
|
|
28754
|
+
const info = parseDrawing(drawing);
|
|
28755
|
+
if (info && info.relationshipId !== null) return info;
|
|
28756
|
+
}
|
|
28757
|
+
const pict = findFirstByTag(branch, "w:pict");
|
|
28758
|
+
if (pict) {
|
|
28759
|
+
const info = parsePict(pict);
|
|
28760
|
+
if (info) return info;
|
|
28761
|
+
}
|
|
28762
|
+
}
|
|
28763
|
+
return null;
|
|
28764
|
+
}
|
|
28698
28765
|
function readPosOffset(containerEl, positionTag) {
|
|
28699
28766
|
const position = findFirstByTag(containerEl, positionTag);
|
|
28700
28767
|
if (!position) return null;
|
|
@@ -42936,6 +43003,24 @@ ${e.toString()}`);
|
|
|
42936
43003
|
}
|
|
42937
43004
|
};
|
|
42938
43005
|
}
|
|
43006
|
+
function imageNode(info, originalXml, marks) {
|
|
43007
|
+
return docxSchema.nodes.image_inline.create(
|
|
43008
|
+
{
|
|
43009
|
+
relationshipId: info.relationshipId,
|
|
43010
|
+
widthEmu: info.widthEmu,
|
|
43011
|
+
heightEmu: info.heightEmu,
|
|
43012
|
+
alt: info.alt,
|
|
43013
|
+
originalXml,
|
|
43014
|
+
wrap: info.wrap,
|
|
43015
|
+
floatSide: info.floatSide,
|
|
43016
|
+
behindDoc: info.behindDoc,
|
|
43017
|
+
offsetXEmu: info.offsetXEmu,
|
|
43018
|
+
offsetYEmu: info.offsetYEmu
|
|
43019
|
+
},
|
|
43020
|
+
void 0,
|
|
43021
|
+
marks
|
|
43022
|
+
);
|
|
43023
|
+
}
|
|
42939
43024
|
function convertRunChild(child, marks, state) {
|
|
42940
43025
|
if (child.kind === "text") {
|
|
42941
43026
|
if (child.value.length === 0) return [];
|
|
@@ -42963,26 +43048,15 @@ ${e.toString()}`);
|
|
|
42963
43048
|
}
|
|
42964
43049
|
if (tag === "w:drawing") {
|
|
42965
43050
|
const info = parseDrawing(child.xml);
|
|
42966
|
-
if (info)
|
|
42967
|
-
|
|
42968
|
-
|
|
42969
|
-
|
|
42970
|
-
|
|
42971
|
-
|
|
42972
|
-
|
|
42973
|
-
|
|
42974
|
-
|
|
42975
|
-
wrap: info.wrap,
|
|
42976
|
-
floatSide: info.floatSide,
|
|
42977
|
-
behindDoc: info.behindDoc,
|
|
42978
|
-
offsetXEmu: info.offsetXEmu,
|
|
42979
|
-
offsetYEmu: info.offsetYEmu
|
|
42980
|
-
},
|
|
42981
|
-
void 0,
|
|
42982
|
-
marks
|
|
42983
|
-
)
|
|
42984
|
-
];
|
|
42985
|
-
}
|
|
43051
|
+
if (info) return [imageNode(info, child.xml, marks)];
|
|
43052
|
+
}
|
|
43053
|
+
if (tag === "mc:AlternateContent") {
|
|
43054
|
+
const info = parseAlternateContent(child.xml);
|
|
43055
|
+
if (info) return [imageNode(info, child.xml, marks)];
|
|
43056
|
+
}
|
|
43057
|
+
if (tag === "w:pict") {
|
|
43058
|
+
const info = parsePict(child.xml);
|
|
43059
|
+
if (info) return [imageNode(info, child.xml, marks)];
|
|
42986
43060
|
}
|
|
42987
43061
|
return [
|
|
42988
43062
|
docxSchema.nodes.opaque_inline.create({ xml: child.xml }, void 0, marks)
|
package/dist/src/cli.js
CHANGED
|
@@ -44955,6 +44955,7 @@ var LoticsClient = class {
|
|
|
44955
44955
|
formData.append("capabilities", JSON.stringify(args.capabilities));
|
|
44956
44956
|
}
|
|
44957
44957
|
formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
|
|
44958
|
+
formData.append("query_aliases", JSON.stringify(args.query_aliases ?? []));
|
|
44958
44959
|
const url2 = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
|
|
44959
44960
|
const response = await fetch(url2, {
|
|
44960
44961
|
method: "POST",
|
|
@@ -47256,10 +47257,35 @@ function undeclaredCapabilities(sourceText, declared) {
|
|
|
47256
47257
|
}
|
|
47257
47258
|
return used;
|
|
47258
47259
|
}
|
|
47259
|
-
|
|
47260
|
+
function driftedQueryAliases(a, b) {
|
|
47261
|
+
const left = a ?? {};
|
|
47262
|
+
const right = b ?? {};
|
|
47263
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].filter((alias) => canonicalJson(left[alias]) !== canonicalJson(right[alias])).sort();
|
|
47264
|
+
}
|
|
47265
|
+
function canonicalJson(value) {
|
|
47266
|
+
const normalize = (v) => {
|
|
47267
|
+
if (Array.isArray(v)) return v.map(normalize);
|
|
47268
|
+
if (v === null || typeof v !== "object") return v;
|
|
47269
|
+
const entries2 = Object.entries(v).sort(
|
|
47270
|
+
([x2], [y]) => x2 < y ? -1 : x2 > y ? 1 : 0
|
|
47271
|
+
);
|
|
47272
|
+
return entries2.map(([key, val]) => [key, normalize(val)]);
|
|
47273
|
+
};
|
|
47274
|
+
return JSON.stringify(normalize(value));
|
|
47275
|
+
}
|
|
47276
|
+
var ALIAS_CALL_HOOKS = {
|
|
47277
|
+
queries: "useQuery",
|
|
47278
|
+
workflows: "useWorkflow",
|
|
47279
|
+
agents: "useAgentRun"
|
|
47280
|
+
};
|
|
47260
47281
|
var LITERAL_ALIAS_ARG = `["'\`]([A-Za-z_$][A-Za-z0-9_$]*)["'\`]`;
|
|
47261
47282
|
function calledAppAliases(sourceText) {
|
|
47262
|
-
const out = {
|
|
47283
|
+
const out = {
|
|
47284
|
+
queries: [],
|
|
47285
|
+
workflows: [],
|
|
47286
|
+
agents: [],
|
|
47287
|
+
dynamic: []
|
|
47288
|
+
};
|
|
47263
47289
|
for (const [kind, hook] of Object.entries(ALIAS_CALL_HOOKS)) {
|
|
47264
47290
|
const seen = /* @__PURE__ */ new Set();
|
|
47265
47291
|
let isDynamic = false;
|
|
@@ -47274,12 +47300,15 @@ function calledAppAliases(sourceText) {
|
|
|
47274
47300
|
}
|
|
47275
47301
|
return out;
|
|
47276
47302
|
}
|
|
47277
|
-
function unboundAliases(
|
|
47278
|
-
const
|
|
47279
|
-
|
|
47303
|
+
function unboundAliases(called, bound) {
|
|
47304
|
+
const missing = (from, against) => {
|
|
47305
|
+
const have = new Set(against ?? []);
|
|
47306
|
+
return [...from ?? []].filter((alias) => !have.has(alias));
|
|
47307
|
+
};
|
|
47280
47308
|
return {
|
|
47281
|
-
|
|
47282
|
-
|
|
47309
|
+
queries: missing(called.queries, bound.queries),
|
|
47310
|
+
workflows: missing(called.workflows, bound.workflows),
|
|
47311
|
+
agents: missing(called.agents, bound.agents)
|
|
47283
47312
|
};
|
|
47284
47313
|
}
|
|
47285
47314
|
|
|
@@ -64923,12 +64952,16 @@ var appWorkflowContractSchema = zod_default.object({
|
|
|
64923
64952
|
inputs: zod_default.record(zod_default.string(), appWorkflowInputSchema).optional(),
|
|
64924
64953
|
outputs: zod_default.record(zod_default.string(), appWorkflowOutputSchema).optional()
|
|
64925
64954
|
});
|
|
64955
|
+
var MAX_APP_CAPABILITY_DESCRIPTION = 300;
|
|
64926
64956
|
var appQueryDeclarationSchema = zod_default.object({
|
|
64927
64957
|
ast: zod_default.unknown().describe(
|
|
64928
64958
|
"Query AST template (a QueryNode). Validated server-side via parseQueryNode at deploy. May embed {{params.<name>}} tokens in filter value positions."
|
|
64929
64959
|
),
|
|
64930
64960
|
params: zod_default.record(zod_default.string(), appWorkflowInputSchema).optional().describe(
|
|
64931
64961
|
"Typed param schema. Keys are param names referenced as {{params.<name>}} in the ast; values declare type + constraints. The server validates the caller's params payload against this before interpolating. Omit for queries that take no params."
|
|
64962
|
+
),
|
|
64963
|
+
description: zod_default.string().optional().describe(
|
|
64964
|
+
`What this query returns, in one line \u2014 read by an agent choosing between the app's aliases. An alias is a JS identifier, which names a query without saying what it covers. Capped at ${MAX_APP_CAPABILITY_DESCRIPTION} characters.`
|
|
64932
64965
|
)
|
|
64933
64966
|
});
|
|
64934
64967
|
var appAgentDeclarationSchema = zod_default.object({
|
|
@@ -70771,18 +70804,17 @@ async function appDeploy(client, args) {
|
|
|
70771
70804
|
);
|
|
70772
70805
|
console.error("Packaging dist...");
|
|
70773
70806
|
await runTar(["-czf", tmpDist, "-C", distDir, "."], projectDir);
|
|
70807
|
+
const liveQueries = await readLiveQueries(client, meta3.app_id);
|
|
70774
70808
|
console.error("Uploading...");
|
|
70775
70809
|
try {
|
|
70776
70810
|
const result = await client.deployAppVersion({
|
|
70811
|
+
queries: liveQueries,
|
|
70777
70812
|
app_id: meta3.app_id,
|
|
70778
70813
|
source_archive: fs4.readFileSync(tmpSource),
|
|
70779
70814
|
dist_archive: fs4.readFileSync(tmpDist),
|
|
70780
70815
|
prev_version_id: meta3.current_version_id,
|
|
70781
70816
|
message: args.message,
|
|
70782
|
-
//
|
|
70783
|
-
// template (parseQueryNode, table access, param coverage).
|
|
70784
|
-
queries: meta3.queries ?? {},
|
|
70785
|
-
// Capabilities are manifest-authoritative (like queries): always send,
|
|
70817
|
+
// Capabilities ARE manifest-authoritative: always send,
|
|
70786
70818
|
// defaulting to `{}` when the manifest declares none — so deleting the
|
|
70787
70819
|
// `capabilities` block turns every capability OFF on the next deploy
|
|
70788
70820
|
// (fail-safe; the declaration is the grant).
|
|
@@ -70795,7 +70827,8 @@ async function appDeploy(client, args) {
|
|
|
70795
70827
|
// typing artifact maintained by habit, so its keys answered this question
|
|
70796
70828
|
// with whatever the author last remembered to write. Stop calling an
|
|
70797
70829
|
// alias + redeploy to lift the guard before removing its binding.
|
|
70798
|
-
workflow_aliases: called.workflows
|
|
70830
|
+
workflow_aliases: called.workflows,
|
|
70831
|
+
query_aliases: called.queries
|
|
70799
70832
|
});
|
|
70800
70833
|
writeAppMeta(projectDir, {
|
|
70801
70834
|
...meta3,
|
|
@@ -70808,6 +70841,7 @@ async function appDeploy(client, args) {
|
|
|
70808
70841
|
const app = await client.getApp(meta3.app_id);
|
|
70809
70842
|
warnIfUnbranded(app);
|
|
70810
70843
|
warnIfUnboundAliases(app, called);
|
|
70844
|
+
warnIfQueriesDiffer(app, meta3.queries ?? {});
|
|
70811
70845
|
} catch (err2) {
|
|
70812
70846
|
console.error(`(skipped post-deploy checks: ${err2.message})`);
|
|
70813
70847
|
}
|
|
@@ -70838,17 +70872,47 @@ function warnIfUnbranded(app) {
|
|
|
70838
70872
|
Find an icon: lotics run search_app_icons '{"query":"<word>"}'`
|
|
70839
70873
|
);
|
|
70840
70874
|
}
|
|
70875
|
+
async function readLiveQueries(client, appId) {
|
|
70876
|
+
try {
|
|
70877
|
+
const app = await client.getApp(appId);
|
|
70878
|
+
return app.queries ?? {};
|
|
70879
|
+
} catch (err2) {
|
|
70880
|
+
console.error(
|
|
70881
|
+
`Could not read ${appId}'s current queries, which this deploy has to send back unchanged: ${err2 instanceof Error ? err2.message : String(err2)}
|
|
70882
|
+
Nothing was deployed. Retry.`
|
|
70883
|
+
);
|
|
70884
|
+
process.exit(1);
|
|
70885
|
+
}
|
|
70886
|
+
}
|
|
70887
|
+
function warnIfQueriesDiffer(app, manifest) {
|
|
70888
|
+
const drifted = driftedQueryAliases(manifest, app.queries);
|
|
70889
|
+
if (drifted.length === 0) return;
|
|
70890
|
+
console.error(
|
|
70891
|
+
`
|
|
70892
|
+
\u26A0 ${drifted.length} quer${drifted.length === 1 ? "y" : "ies"} in package.json differ from the app: ${drifted.join(", ")}
|
|
70893
|
+
A deploy does not write them. Push yours with 'lotics app query set --all'
|
|
70894
|
+
(or one alias at a time), or adopt the app's with 'lotics app pull'.`
|
|
70895
|
+
);
|
|
70896
|
+
}
|
|
70841
70897
|
function warnIfUnboundAliases(app, called) {
|
|
70842
|
-
const {
|
|
70898
|
+
const {
|
|
70899
|
+
queries: unboundQueries,
|
|
70900
|
+
workflows: unboundWorkflows,
|
|
70901
|
+
agents: unboundAgents
|
|
70902
|
+
} = unboundAliases(called, {
|
|
70903
|
+
queries: Object.keys(app.queries ?? {}),
|
|
70843
70904
|
workflows: Object.keys(app.workflows ?? {}),
|
|
70844
70905
|
agents: Object.keys(app.agents ?? {})
|
|
70845
70906
|
});
|
|
70846
|
-
if (unboundWorkflows.length === 0 && unboundAgents.length === 0)
|
|
70907
|
+
if (unboundQueries.length === 0 && unboundWorkflows.length === 0 && unboundAgents.length === 0) {
|
|
70908
|
+
return;
|
|
70909
|
+
}
|
|
70847
70910
|
const lines = [
|
|
70848
|
-
"\n\u26A0 This app CALLS aliases that are NOT bound on the server.
|
|
70849
|
-
'
|
|
70850
|
-
"
|
|
70911
|
+
"\n\u26A0 This app CALLS aliases that are NOT bound on the server. A deploy ships code and",
|
|
70912
|
+
' binds nothing, so the app will throw "has no \u2026 alias" the first time it calls them.',
|
|
70913
|
+
" Bind each one:"
|
|
70851
70914
|
];
|
|
70915
|
+
for (const alias of unboundQueries) lines.push(` \u2022 query "${alias}" \u2192 lotics app query set ${alias}`);
|
|
70852
70916
|
for (const alias of unboundWorkflows) lines.push(` \u2022 workflow "${alias}" \u2192 lotics app workflow set ${alias}`);
|
|
70853
70917
|
for (const alias of unboundAgents) lines.push(` \u2022 agent "${alias}" \u2192 bind with set_app_agent (lotics run set_app_agent \u2026)`);
|
|
70854
70918
|
console.error(lines.join("\n"));
|
|
@@ -71158,20 +71222,31 @@ async function appWorkflowSet(client, args) {
|
|
|
71158
71222
|
async function appQuerySet(client, args) {
|
|
71159
71223
|
const projectDir = process.cwd();
|
|
71160
71224
|
const meta3 = readAppMeta(projectDir);
|
|
71161
|
-
const
|
|
71162
|
-
|
|
71163
|
-
|
|
71164
|
-
|
|
71165
|
-
|
|
71166
|
-
process.exit(1);
|
|
71225
|
+
const declared = meta3.queries ?? {};
|
|
71226
|
+
const aliases = "all" in args ? Object.keys(declared).sort() : [args.alias];
|
|
71227
|
+
if (aliases.length === 0) {
|
|
71228
|
+
console.error("No queries in package.json#lotics.queries \u2014 nothing to push.");
|
|
71229
|
+
return;
|
|
71167
71230
|
}
|
|
71168
|
-
const
|
|
71169
|
-
|
|
71170
|
-
|
|
71171
|
-
|
|
71231
|
+
const pushed = [];
|
|
71232
|
+
for (const alias of aliases) {
|
|
71233
|
+
const declaration = declared[alias];
|
|
71234
|
+
if (!declaration) {
|
|
71235
|
+
console.error(
|
|
71236
|
+
`No query "${alias}" in package.json#lotics.queries. Declare it there (alias \u2192 { ast, params? }) first.`
|
|
71237
|
+
);
|
|
71238
|
+
process.exit(1);
|
|
71239
|
+
}
|
|
71240
|
+
const res = await client.setAppQuery(meta3.app_id, alias, declaration);
|
|
71241
|
+
if (res.error) {
|
|
71242
|
+
if (pushed.length > 0) console.error(`Pushed before the failure: ${pushed.join(", ")}.`);
|
|
71243
|
+
console.error(`Failed to set query "${alias}": ${res.error}`);
|
|
71244
|
+
process.exit(1);
|
|
71245
|
+
}
|
|
71246
|
+
pushed.push(alias);
|
|
71172
71247
|
}
|
|
71173
71248
|
console.error(
|
|
71174
|
-
`Set
|
|
71249
|
+
`Set ${pushed.length} quer${pushed.length === 1 ? "y" : "ies"} on ${meta3.app_id}: ${pushed.join(", ")}.`
|
|
71175
71250
|
);
|
|
71176
71251
|
}
|
|
71177
71252
|
async function appWorkflowPull(client) {
|
|
@@ -89579,37 +89654,47 @@ function extractParagraphText(el) {
|
|
|
89579
89654
|
}
|
|
89580
89655
|
return parts.join("");
|
|
89581
89656
|
}
|
|
89582
|
-
function
|
|
89583
|
-
const
|
|
89657
|
+
function getParagraphTextSegments(el) {
|
|
89658
|
+
const segments = [];
|
|
89584
89659
|
for (const child of getChildren(el)) {
|
|
89585
89660
|
if (getTagName(child) !== "w:r") continue;
|
|
89586
|
-
let
|
|
89587
|
-
|
|
89661
|
+
let pending = [];
|
|
89662
|
+
const flush = () => {
|
|
89663
|
+
if (pending.length === 0) return;
|
|
89664
|
+
const text = pending.map(
|
|
89665
|
+
(node) => getChildren(node).reduce(
|
|
89666
|
+
(acc, tn) => typeof tn["#text"] === "string" ? acc + tn["#text"] : acc,
|
|
89667
|
+
""
|
|
89668
|
+
)
|
|
89669
|
+
).join("");
|
|
89670
|
+
segments.push({ element: child, textNodes: pending, text });
|
|
89671
|
+
pending = [];
|
|
89672
|
+
};
|
|
89588
89673
|
for (const grandchild of getChildren(child)) {
|
|
89589
|
-
if (getTagName(grandchild)
|
|
89590
|
-
|
|
89591
|
-
|
|
89592
|
-
if (typeof tn["#text"] === "string") text += tn["#text"];
|
|
89674
|
+
if (getTagName(grandchild) === "w:t") {
|
|
89675
|
+
pending.push(grandchild);
|
|
89676
|
+
continue;
|
|
89593
89677
|
}
|
|
89678
|
+
flush();
|
|
89594
89679
|
}
|
|
89595
|
-
|
|
89680
|
+
flush();
|
|
89596
89681
|
}
|
|
89597
|
-
return
|
|
89682
|
+
return segments;
|
|
89598
89683
|
}
|
|
89599
|
-
function
|
|
89600
|
-
const children = getChildren(
|
|
89601
|
-
const textNode = {
|
|
89602
|
-
|
|
89603
|
-
|
|
89604
|
-
|
|
89605
|
-
|
|
89684
|
+
function setSegmentText(segment, newText) {
|
|
89685
|
+
const children = getChildren(segment.element);
|
|
89686
|
+
const textNode = {
|
|
89687
|
+
"w:t": [{ "#text": newText }],
|
|
89688
|
+
":@": { "@_xml:space": "preserve" }
|
|
89689
|
+
};
|
|
89690
|
+
const head = children.indexOf(segment.textNodes[0]);
|
|
89691
|
+
children[head] = textNode;
|
|
89692
|
+
for (let i2 = segment.textNodes.length - 1; i2 >= 1; i2--) {
|
|
89693
|
+
const at2 = children.indexOf(segment.textNodes[i2]);
|
|
89694
|
+
if (at2 >= 0) children.splice(at2, 1);
|
|
89606
89695
|
}
|
|
89607
|
-
|
|
89608
|
-
|
|
89609
|
-
}
|
|
89610
|
-
function setRunText(run, newText) {
|
|
89611
|
-
writeRunText(run.element, newText);
|
|
89612
|
-
run.text = newText;
|
|
89696
|
+
segment.textNodes = [textNode];
|
|
89697
|
+
segment.text = newText;
|
|
89613
89698
|
}
|
|
89614
89699
|
function computeReplacementRanges(text, query, replacement, matchType, maxReplacements, startCount) {
|
|
89615
89700
|
const ranges = [];
|
|
@@ -89644,39 +89729,39 @@ function computeReplacementRanges(text, query, replacement, matchType, maxReplac
|
|
|
89644
89729
|
}
|
|
89645
89730
|
return { ranges, count };
|
|
89646
89731
|
}
|
|
89647
|
-
function applyRangeReplacements(
|
|
89732
|
+
function applyRangeReplacements(segments, ranges) {
|
|
89648
89733
|
if (ranges.length === 0) return;
|
|
89649
89734
|
const bounds = [];
|
|
89650
89735
|
let offset = 0;
|
|
89651
|
-
for (const
|
|
89652
|
-
bounds.push({
|
|
89653
|
-
offset +=
|
|
89736
|
+
for (const segment of segments) {
|
|
89737
|
+
bounds.push({ segment, start: offset, end: offset + segment.text.length });
|
|
89738
|
+
offset += segment.text.length;
|
|
89654
89739
|
}
|
|
89655
|
-
const joined =
|
|
89740
|
+
const joined = segments.map((s) => s.text).join("");
|
|
89656
89741
|
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
89657
|
-
for (const {
|
|
89742
|
+
for (const { segment, start: segStart, end: segEnd } of bounds) {
|
|
89658
89743
|
let next = "";
|
|
89659
|
-
let cursor =
|
|
89744
|
+
let cursor = segStart;
|
|
89660
89745
|
for (const range2 of sorted) {
|
|
89661
|
-
if (range2.end <=
|
|
89662
|
-
const keepUntil = Math.min(range2.start,
|
|
89663
|
-
const from = Math.max(cursor,
|
|
89746
|
+
if (range2.end <= segStart || range2.start >= segEnd) continue;
|
|
89747
|
+
const keepUntil = Math.min(range2.start, segEnd);
|
|
89748
|
+
const from = Math.max(cursor, segStart);
|
|
89664
89749
|
if (keepUntil > from) next += joined.slice(from, keepUntil);
|
|
89665
|
-
if (
|
|
89750
|
+
if (segStart <= range2.start && range2.start < segEnd) next += range2.replacement;
|
|
89666
89751
|
cursor = Math.max(cursor, range2.end);
|
|
89667
89752
|
}
|
|
89668
|
-
const tail = Math.max(cursor,
|
|
89669
|
-
if (
|
|
89670
|
-
if (next !==
|
|
89753
|
+
const tail = Math.max(cursor, segStart);
|
|
89754
|
+
if (segEnd > tail) next += joined.slice(tail, segEnd);
|
|
89755
|
+
if (next !== segment.text) setSegmentText(segment, next);
|
|
89671
89756
|
}
|
|
89672
89757
|
}
|
|
89673
89758
|
function replaceInParagraph(el, query, replacement, matchType, maxReplacements, replacementsMade) {
|
|
89674
89759
|
if (!extractParagraphText(el)) return replacementsMade;
|
|
89675
|
-
const
|
|
89676
|
-
if (
|
|
89677
|
-
const text =
|
|
89760
|
+
const segments = getParagraphTextSegments(el);
|
|
89761
|
+
if (segments.length === 0) return replacementsMade;
|
|
89762
|
+
const text = segments.length === 1 ? segments[0].text : segments.map((s) => s.text).join("");
|
|
89678
89763
|
const { ranges, count } = computeReplacementRanges(text, query, replacement, matchType, maxReplacements, replacementsMade);
|
|
89679
|
-
applyRangeReplacements(
|
|
89764
|
+
applyRangeReplacements(segments, ranges);
|
|
89680
89765
|
return count;
|
|
89681
89766
|
}
|
|
89682
89767
|
function replaceText(bodyElements, query, replacement, matchType = "contains", maxReplacements = Infinity) {
|
|
@@ -92707,11 +92792,15 @@ Available workspaces:`);
|
|
|
92707
92792
|
if (subcommand === "query") {
|
|
92708
92793
|
const action = toolArgs;
|
|
92709
92794
|
if (action === "set") {
|
|
92710
|
-
|
|
92795
|
+
if (restArgs.includes("--all")) {
|
|
92796
|
+
await appQuerySet(client, { all: true });
|
|
92797
|
+
return;
|
|
92798
|
+
}
|
|
92799
|
+
const alias = restArgs.find((a) => !a.startsWith("--"));
|
|
92711
92800
|
if (!alias) {
|
|
92712
|
-
console.error("Usage: lotics app query set <alias>");
|
|
92801
|
+
console.error("Usage: lotics app query set <alias> | --all");
|
|
92713
92802
|
console.error(
|
|
92714
|
-
"Pushes package.json#lotics.queries
|
|
92803
|
+
"Pushes package.json#lotics.queries to apps.queries via set_app_query. A deploy does not."
|
|
92715
92804
|
);
|
|
92716
92805
|
process.exit(1);
|
|
92717
92806
|
}
|
|
@@ -92720,7 +92809,10 @@ Available workspaces:`);
|
|
|
92720
92809
|
}
|
|
92721
92810
|
console.error("Usage:");
|
|
92722
92811
|
console.error(
|
|
92723
|
-
" lotics app query set <alias> Push package.json#lotics.queries.<alias> to apps.queries
|
|
92812
|
+
" lotics app query set <alias> Push package.json#lotics.queries.<alias> to apps.queries"
|
|
92813
|
+
);
|
|
92814
|
+
console.error(
|
|
92815
|
+
" lotics app query set --all Push every declared query (a deploy does not write them)"
|
|
92724
92816
|
);
|
|
92725
92817
|
process.exit(1);
|
|
92726
92818
|
}
|
package/dist/src/client.d.ts
CHANGED
|
@@ -222,6 +222,7 @@ export declare class LoticsClient {
|
|
|
222
222
|
queries?: Record<string, {
|
|
223
223
|
ast: unknown;
|
|
224
224
|
params?: Record<string, unknown>;
|
|
225
|
+
description?: string;
|
|
225
226
|
}> | null;
|
|
226
227
|
/**
|
|
227
228
|
* Live alias → agent declaration map from `apps.agents`. Source of truth for
|
|
@@ -866,6 +867,7 @@ export declare class LoticsClient {
|
|
|
866
867
|
setAppQuery(app_id: string, alias: string, declaration: {
|
|
867
868
|
ast: unknown;
|
|
868
869
|
params?: Record<string, unknown>;
|
|
870
|
+
description?: string;
|
|
869
871
|
}): Promise<ToolExecuteResult>;
|
|
870
872
|
/**
|
|
871
873
|
* Bind (create or replace) an app agent by alias via the `set_app_agent` tool
|
|
@@ -1006,14 +1008,16 @@ export declare class LoticsClient {
|
|
|
1006
1008
|
prev_version_id?: string | null;
|
|
1007
1009
|
message?: string | null;
|
|
1008
1010
|
/**
|
|
1009
|
-
* Alias → query declaration map from
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1011
|
+
* Alias → query declaration map ECHOED BACK from the live row (see
|
|
1012
|
+
* `readLiveQueries`), not read from the manifest. Every key the row holds
|
|
1013
|
+
* must survive the round trip — `description` included — because a server
|
|
1014
|
+
* that treats a present map as authoritative writes exactly what it is
|
|
1015
|
+
* sent, so a lossy echo silently strips whatever it forgot.
|
|
1013
1016
|
*/
|
|
1014
1017
|
queries?: Record<string, {
|
|
1015
1018
|
ast: unknown;
|
|
1016
1019
|
params?: Record<string, unknown>;
|
|
1020
|
+
description?: string;
|
|
1017
1021
|
}>;
|
|
1018
1022
|
/**
|
|
1019
1023
|
* Opt-in app capabilities from `package.json#lotics.capabilities`. The CLI
|
|
@@ -1032,6 +1036,7 @@ export declare class LoticsClient {
|
|
|
1032
1036
|
* (empty array when none declared).
|
|
1033
1037
|
*/
|
|
1034
1038
|
workflow_aliases?: string[];
|
|
1039
|
+
query_aliases?: string[];
|
|
1035
1040
|
}): Promise<{
|
|
1036
1041
|
version_id: string;
|
|
1037
1042
|
version_number: number;
|
package/dist/src/client.js
CHANGED
|
@@ -723,6 +723,7 @@ export class LoticsClient {
|
|
|
723
723
|
// server records what the served version calls — the remove_app_workflow
|
|
724
724
|
// guard reads this back. These are the manifest KEYS only, never bindings.
|
|
725
725
|
formData.append("workflow_aliases", JSON.stringify(args.workflow_aliases ?? []));
|
|
726
|
+
formData.append("query_aliases", JSON.stringify(args.query_aliases ?? []));
|
|
726
727
|
const url = `${this.baseUrl}/v1/apps/${encodeURIComponent(args.app_id)}/versions`;
|
|
727
728
|
const response = await fetch(url, {
|
|
728
729
|
method: "POST",
|
package/docs/cli_reference.md
CHANGED
|
@@ -30,13 +30,13 @@ Per-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. S
|
|
|
30
30
|
| `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |
|
|
31
31
|
| `lotics app create <name> [path]` | Scaffold a Vite+React+TS custom-code app project; POST /v1/apps; npm install; vite build; upload as v1 |
|
|
32
32
|
| `lotics app pull <app_id> [path]` | Download source archive from R2 (presigned), extract, npm install, stamp package.json's `lotics` field. With no `[path]`: refresh the cwd IN PLACE when it's already this app's own project (its manifest `app_id` matches — the documented `cd <app> && lotics app pull` flow), else clone into an `<name>/` subdir; this avoids the stray nested `./<name>/` subdir a pull-from-inside-the-app used to drop. — `workflows` and `agents` are sourced from the live App row (NOT the archived manifest), so `set_app_workflow` / `set_app_agent` authoring survives the pull. Regenerates `.lotics/app_{workflows,queries,agents}.d.ts` so `useWorkflow` / `useQuery` / `useAgentRun` stay typed. Also writes one `src/workflows/<alias>.ts` per bound workflow (faithful body from `get_app_workflow`) and one `src/agents/<alias>.md` per bound agent (its instructions, straight off the live row) — so the prose an author actually edits lives in a file, and pull always overwrites it from live, leaving no second copy to drift. A legacy workflow alias with no rendered source, or an agent with no instructions, warns and is skipped. The stamped `lotics.agents` map carries the TYPED half only (`inputs`/`outputs`/`tool_names`/`model_id`/…) — an agent's prose lives solely in its `.md`, so there is never a second local copy to desync; a stale `instructions` left by an older CLI is inert and disappears on the next pull |
|
|
33
|
-
| `lotics app deploy -m <message>` | **`-m` is REQUIRED** (CLI errors without a non-empty message) — each deploy is a version row read back by `lotics app versions`, so a blank message loses the audit trail. npm run build; tar source + dist; POST /v1/apps/{id}/versions multipart. Carries code +
|
|
33
|
+
| `lotics app deploy -m <message>` | **`-m` is REQUIRED** (CLI errors without a non-empty message) — each deploy is a version row read back by `lotics app versions`, so a blank message loses the audit trail. npm run build; tar source + dist; POST /v1/apps/{id}/versions multipart. Carries code + capabilities only — **neither queries nor workflow/agent bindings are a deploy concern** (`set_app_workflow` / `remove_app_workflow` own `apps.workflows`; the manifest's `workflows` map is a pulled reflection used only for `useWorkflow` codegen). Deploy DOES send the manifest's `lotics.workflows` alias KEYS (not the bindings) as `workflow_aliases`, recorded on the version row so `remove_app_workflow` can refuse to unbind an alias the served version still declares. It also reports any `lotics.queries` alias whose declaration DIFFERS from the app's, naming both recoveries (`app query set --all` to push yours, `app pull` to adopt the app's) — a deploy no longer writes them, so the two are allowed to drift. After a successful deploy it **warns loudly about any alias the source CALLS that is NOT bound on the server** (a `getApp` diff via `warnIfUnboundAliases`) — since deploy never binds them, that would otherwise throw only at the app's first `useWorkflow` / `useAgentRun` call; the warning points to `lotics app workflow set` / `set_app_agent`. Advisory only (never fails the deploy). |
|
|
34
34
|
| `lotics app versions [app_id]` | `GET /v1/apps/{id}/versions` — print deploy history newest-first (version number, timestamp, deployer name, build status, the `-m` message; `*` marks the currently-served version). app_id from the local manifest, or pass one to inspect any app without pulling it. Admin-only server-side (mirrors deploy + source download). Answers "what shipped, when, by whom" — e.g. whether a fix was live at an incident's time. The deploy pipeline already persisted all of this in `app_versions`; this is the read surface. Title → stderr, table → stdout (pipeable). |
|
|
35
35
|
| `lotics app codegen [path]` | Regenerate `.lotics/*` from the manifest + workspace schema **without a deploy**. The three `.d.ts` companions (`app_{workflows,queries,agents}.d.ts`) are always rewritten (synchronous, no network). When credentials resolve, also rewrites the **runtime** `.lotics/app_fields.ts` — **branched on whether the app is a package installation** (`getApp().package_id` set, from `generate_package_fields.ts`): a **linked/published** app emits the BINDING form (`F`/`OPT`/`ROLE` resolved from the installation's LIVE binding — via `appBinding` / the `binding` RPC — at module load through `getAppBinding()` + top-level await, so the source stays portable across every install); a **bespoke** app emits the BAKED form (`generate_app_fields.ts`) — a real `.ts` exporting `F` (table→field→`"fld_…"`) + `OPT` (table→select-field→option→`"opt_…"`) keyed by display-name aliases, for the tables the app's queries reference (+ optional `package.json#lotics.codegen.tables` allowlist). Both forms share the `F`/`OPT` shape (contract aliases derive from the same slugified display names), so a published origin's deployed source compiles unchanged. Writing the BINDING form also heals the project's vitest setup (`ensureAppVitestSetup`, folded into the same write boundary): the binding form awaits `getAppBinding()` (a network call) at module load, so without a stub `npm test` fails to collect any test that imports the app graph — the heal writes `vitest.setup.ts` (mocks only `getAppBinding`, returning an echo binding: any alias → a self-identifying `fld:test:…`/`opt:test:…`/`grp:test:…` id) if absent, and warns the one-liner to add to `vite.config.ts`'s `test.setupFiles` if the wiring is missing (TS source isn't safely munged, mirroring `ensureAppTsconfig`'s JSONC-tsconfig warn). New scaffolds ship both. Also refreshes each bound workflow's `.lotics/workflows/<alias>.globals.d.ts` + re-wraps its EXISTING `src/workflows/<alias>.ts` body in the current envelope (strips + re-wraps; never re-fetches the body, so local edits survive). A getApp / binding / schema / dts-fetch failure is non-fatal (warns, keeps the last-generated files). |
|
|
36
36
|
| `lotics app workflow run <alias> '<json>'` | Execute a bound app workflow end-to-end via `appWorkflow`. `app_id` comes from the local manifest; the alias must be bound (`set_app_workflow`). Inputs ingest exactly like `lotics run` (inline JSON / `@file` / stdin — bulk inputs bypass `ARG_MAX`). Prints the full `{status,message,data,files,side_effects}` JSON to stdout + a one-line summary to stderr; exits non-zero on `status:"error"` (assertable). `--print-created` (alias `--report-effects`) renders the honest post-run harvest (GAP-58): created records grouped by table, a paste-ready `lotics run delete_records …` per table, then the **mandatory caveat** naming what cannot be auto-undone (external integrations + notifications) and that sub-workflows may have run. `--cleanup` (DEFAULT OFF, implies the report) additionally runs the deletes for harvested records ONLY — never files / external / notifications. Neither is a rollback — a rollback is structurally impossible here. |
|
|
37
37
|
| `lotics app workflow set <alias>` | Push the edited `src/workflows/<alias>.ts` body through `set_app_workflow` (the single author of `apps.workflows`). Reads the body from disk (header + `/// <reference>` + `export {};` marker + the `__workflow` wrapper all stripped) + the typed `inputs`/`outputs` from `package.json#lotics.workflows.<alias>`; the **server** re-verifies the body and echoes the bound `outputs` (declared, else DERIVED from `return({ data })`). When the manifest declared NO `outputs`, the DERIVED echo is written back into `package.json#lotics.workflows.<alias>.outputs` (a SURGICAL write — preserves `knowledge`/`config` and every other manifest field) and that alias's types are refreshed in place, so `useWorkflow("<alias>")`'s `result.data` is typed immediately with no hand-copy and no second `lotics app codegen`; an explicitly-declared `outputs` is authoritative and never overwritten. Deploy still never authors workflows — this is a CLI convenience over the existing tool. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a verify failure. |
|
|
38
38
|
| `lotics app agent set <alias>` | Push the edited `src/agents/<alias>.md` instructions back through `set_app_agent` — the agent mirror of `app workflow set`, and the deploy-free authoring path for `apps.agents`. Reads the prose from disk (the `<!-- lotics: … -->` header stripped) and the typed fields (`inputs`/`outputs`/`tool_names`/`model_id`/`effort_level`/`knowledge_doc_ids`/`query_aliases`/`workflow_aliases`) from `package.json#lotics.agents.<alias>`, then sends them as ONE declaration. That assembly is the point: **`set_app_agent` REPLACES the declaration rather than patching it**, so a hand-built payload that sets one field silently drops the instructions, the output schema and the model pin — a silent, unrecoverable edit against a live prompt. Clear error + non-zero exit on a missing file, an alias absent from the manifest, or a file that is empty once the header is stripped (refusing to push an empty prompt). `app pull` writes the file; edit, then `set`. |
|
|
39
|
-
| `lotics app query set <alias>` | Push `package.json#lotics.queries
|
|
39
|
+
| `lotics app query set <alias>` \| `--all` | Push `package.json#lotics.queries` (`{ ast, params? }` per alias) to `apps.queries` through `set_app_query` — **the only author of a query binding**, the mirror of `app workflow set`. A deploy ships code and binds nothing. The **server** validates each one exactly as it always did (alias identifier, workspace-only tables, resolvable fields, declared params). `--all` pushes every declared alias, alias-sorted, stopping at the first failure and naming what already landed. Clear error + non-zero exit on an alias absent from the manifest or a validation failure. |
|
|
40
40
|
| `lotics app agent run <app_id> <alias> ['<json>'\|@file\|stdin]` | Run a bound app agent end-to-end (GAP-87). A run needs no deployed UI bundle — just the app row + the bound agent declaration + member auth — so the **`app_id` is explicit** (not read from a local manifest). Inputs ingest exactly like `lotics run` (inline JSON / `@file` / stdin; empty = `{}`). Opens the run's SSE (`appAgentRunStream`), streams `text-delta` prose to **stderr** as live progress, then reports from the **settled run RECORD** (`listAgentRuns`, polled to a terminal status — the client stream can close a beat before the run settles, or drop while it runs on server-side): default prints the run's structured `output` (JSON) or final text to **stdout** + a status line to stderr; `--json` prints the full run summary to stdout. Selects THIS run by the `x-app-agent-run-id` header (ordering-independent). Exits 0 **only** when the settled status is `completed`; otherwise non-zero with the run's error surfaced. A settled run that never appears fails loudly (never a silent success). A fresh `session_id` is minted per run (self-contained); `--session <id>` continues an existing thread (prior runs become the agent's context). |
|
|
41
41
|
| `lotics app workflow pull` | Rewrite every `src/workflows/<alias>.ts` from the server (faithful body per bound alias via `get_app_workflow`) **+ its `.lotics/workflows/<alias>.globals.d.ts`** (via `getAppWorkflowDts`, so the body is locally typecheckable via `lotics app workflow check`) without a full `app pull` (no source archive, no npm install). A legacy alias with no rendered source warns and is skipped; a dts-fetch failure is non-fatal (body still written with the fallback wrapper, typecheck degraded). Also idempotently patches the main `tsconfig.json` `exclude` to cover `src/workflows` + `.lotics/workflows` so a pre-existing app's `npm run typecheck` never loads the bodies or the colliding per-alias globals. |
|
|
42
42
|
| `lotics app workflow check [alias]` | Check the editable workflow bodies locally, no auth / no network, in the **server's own order** — parse, then type-check. **Parse** runs `parseWorkflowJs` from `@lotics/shared` (the SAME module `verifyWorkflow` calls, never a second implementation — that is what let the two diverge once) over the stripped body `set` would upload, with `toolNames: undefined` (the CLI ships no tool registry, so tool-name resolution stays a server check while every shape/scope rule runs here). A body the subset rejects reports **that error alone** and skips the compiler — it never reaches the server's compiler either, so tsc's opinion of it is noise. **Type-check** then builds an **isolated** `ts.Program` per alias from exactly that alias's `{body, globals}` pair — mirroring the server, which verifies one body at a time — so the per-alias ambient `trigger` never collides and `trigger.app_workflow.inputs` is checked against the right alias (GAP-59). All aliases run in ONE node process (N programs, not N `tsc` spawns), with the SAME compile options the server uses at set-time verify (lib `es2022` with no DOM, target ES2022, strict, NodeNext, `types:[]`, skipLibCheck) and the app's OWN `typescript` (resolved from its `node_modules`, never bundled into the CLI). What the compiler sees is the **checked source**, not the file: `rewriteAccumulatorAppends` from `@lotics/shared` — the SAME transform the server applies before its set-time compile — is applied in memory, so a pulled body's canonical `out = concat(out, [item])` accumulator checks green here exactly as it saves there (compiling the raw text went red on it), and the body on disk is never rewritten. Reports `<file>:<line>:<col> - <TS####\|subset>` at the **physical** line in `src/workflows/<alias>.ts`, so an editor jump lands on the offending code (these are deliberately NOT `set`'s body-relative numbers — `set` prints no file path, so there is no format to agree with); exits non-zero if any alias fails. Green is honest but not total: `set` additionally resolves names, lints and structurally validates against the live workspace — passes that need its tables and tool schemas, so they cannot run offline, and the success line says so. A bound alias with no body file yet warns + skips; a body with no globals errors (run a pull). |
|