@salesforce/graphiti 10.10.2

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.
Files changed (282) hide show
  1. package/AGENT_GUIDE.md +424 -0
  2. package/CHANGELOG.md +448 -0
  3. package/LICENSE.txt +82 -0
  4. package/README.md +204 -0
  5. package/TASK.md +249 -0
  6. package/dist/cli.d.ts +7 -0
  7. package/dist/cli.js +683 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/args.d.ts +13 -0
  10. package/dist/commands/args.js +207 -0
  11. package/dist/commands/args.js.map +1 -0
  12. package/dist/commands/build.d.ts +11 -0
  13. package/dist/commands/build.js +209 -0
  14. package/dist/commands/build.js.map +1 -0
  15. package/dist/commands/connect.d.ts +8 -0
  16. package/dist/commands/connect.js +55 -0
  17. package/dist/commands/connect.js.map +1 -0
  18. package/dist/commands/describe.d.ts +6 -0
  19. package/dist/commands/describe.js +229 -0
  20. package/dist/commands/describe.js.map +1 -0
  21. package/dist/commands/meta.d.ts +9 -0
  22. package/dist/commands/meta.js +140 -0
  23. package/dist/commands/meta.js.map +1 -0
  24. package/dist/commands/navigate.d.ts +14 -0
  25. package/dist/commands/navigate.js +105 -0
  26. package/dist/commands/navigate.js.map +1 -0
  27. package/dist/commands/query-helpers.d.ts +80 -0
  28. package/dist/commands/query-helpers.js +865 -0
  29. package/dist/commands/query-helpers.js.map +1 -0
  30. package/dist/commands/query.d.ts +26 -0
  31. package/dist/commands/query.js +901 -0
  32. package/dist/commands/query.js.map +1 -0
  33. package/dist/commands/review.d.ts +18 -0
  34. package/dist/commands/review.js +533 -0
  35. package/dist/commands/review.js.map +1 -0
  36. package/dist/commands/session-mgmt.d.ts +25 -0
  37. package/dist/commands/session-mgmt.js +206 -0
  38. package/dist/commands/session-mgmt.js.map +1 -0
  39. package/dist/commands/type.d.ts +6 -0
  40. package/dist/commands/type.js +342 -0
  41. package/dist/commands/type.js.map +1 -0
  42. package/dist/commands/validate-input.d.ts +6 -0
  43. package/dist/commands/validate-input.js +32 -0
  44. package/dist/commands/validate-input.js.map +1 -0
  45. package/dist/intent/build-aggregate.d.ts +33 -0
  46. package/dist/intent/build-aggregate.js +134 -0
  47. package/dist/intent/build-aggregate.js.map +1 -0
  48. package/dist/intent/build-create.d.ts +14 -0
  49. package/dist/intent/build-create.js +16 -0
  50. package/dist/intent/build-create.js.map +1 -0
  51. package/dist/intent/build-delete.d.ts +30 -0
  52. package/dist/intent/build-delete.js +53 -0
  53. package/dist/intent/build-delete.js.map +1 -0
  54. package/dist/intent/build-detail.d.ts +32 -0
  55. package/dist/intent/build-detail.js +80 -0
  56. package/dist/intent/build-detail.js.map +1 -0
  57. package/dist/intent/build-discover.d.ts +30 -0
  58. package/dist/intent/build-discover.js +149 -0
  59. package/dist/intent/build-discover.js.map +1 -0
  60. package/dist/intent/build-list.d.ts +28 -0
  61. package/dist/intent/build-list.js +75 -0
  62. package/dist/intent/build-list.js.map +1 -0
  63. package/dist/intent/build-mutation.d.ts +23 -0
  64. package/dist/intent/build-mutation.js +54 -0
  65. package/dist/intent/build-mutation.js.map +1 -0
  66. package/dist/intent/build-output.d.ts +27 -0
  67. package/dist/intent/build-output.js +60 -0
  68. package/dist/intent/build-output.js.map +1 -0
  69. package/dist/intent/build-raw.d.ts +23 -0
  70. package/dist/intent/build-raw.js +54 -0
  71. package/dist/intent/build-raw.js.map +1 -0
  72. package/dist/intent/build-update.d.ts +14 -0
  73. package/dist/intent/build-update.js +16 -0
  74. package/dist/intent/build-update.js.map +1 -0
  75. package/dist/intent/get-schema-with-priming.d.ts +26 -0
  76. package/dist/intent/get-schema-with-priming.js +32 -0
  77. package/dist/intent/get-schema-with-priming.js.map +1 -0
  78. package/dist/intent/select-child-relationship.d.ts +19 -0
  79. package/dist/intent/select-child-relationship.js +38 -0
  80. package/dist/intent/select-child-relationship.js.map +1 -0
  81. package/dist/intent/types.d.ts +159 -0
  82. package/dist/intent/types.js +21 -0
  83. package/dist/intent/types.js.map +1 -0
  84. package/dist/lib/apply-command.d.ts +15 -0
  85. package/dist/lib/apply-command.js +238 -0
  86. package/dist/lib/apply-command.js.map +1 -0
  87. package/dist/lib/auth.d.ts +38 -0
  88. package/dist/lib/auth.js +113 -0
  89. package/dist/lib/auth.js.map +1 -0
  90. package/dist/lib/codegen.d.ts +32 -0
  91. package/dist/lib/codegen.js +700 -0
  92. package/dist/lib/codegen.js.map +1 -0
  93. package/dist/lib/command-registry.d.ts +59 -0
  94. package/dist/lib/command-registry.js +366 -0
  95. package/dist/lib/command-registry.js.map +1 -0
  96. package/dist/lib/formatter.d.ts +76 -0
  97. package/dist/lib/formatter.js +419 -0
  98. package/dist/lib/formatter.js.map +1 -0
  99. package/dist/lib/fs-utils.d.ts +23 -0
  100. package/dist/lib/fs-utils.js +46 -0
  101. package/dist/lib/fs-utils.js.map +1 -0
  102. package/dist/lib/graphql-name.d.ts +27 -0
  103. package/dist/lib/graphql-name.js +32 -0
  104. package/dist/lib/graphql-name.js.map +1 -0
  105. package/dist/lib/interactive.d.ts +6 -0
  106. package/dist/lib/interactive.js +562 -0
  107. package/dist/lib/interactive.js.map +1 -0
  108. package/dist/lib/introspect.d.ts +40 -0
  109. package/dist/lib/introspect.js +280 -0
  110. package/dist/lib/introspect.js.map +1 -0
  111. package/dist/lib/object-info.d.ts +79 -0
  112. package/dist/lib/object-info.js +313 -0
  113. package/dist/lib/object-info.js.map +1 -0
  114. package/dist/lib/path-selection.d.ts +50 -0
  115. package/dist/lib/path-selection.js +146 -0
  116. package/dist/lib/path-selection.js.map +1 -0
  117. package/dist/lib/prime-schema.d.ts +59 -0
  118. package/dist/lib/prime-schema.js +158 -0
  119. package/dist/lib/prime-schema.js.map +1 -0
  120. package/dist/lib/query-builder.d.ts +10 -0
  121. package/dist/lib/query-builder.js +168 -0
  122. package/dist/lib/query-builder.js.map +1 -0
  123. package/dist/lib/query-commands.d.ts +19 -0
  124. package/dist/lib/query-commands.js +262 -0
  125. package/dist/lib/query-commands.js.map +1 -0
  126. package/dist/lib/session.d.ts +205 -0
  127. package/dist/lib/session.js +1075 -0
  128. package/dist/lib/session.js.map +1 -0
  129. package/dist/lib/tokenize.d.ts +12 -0
  130. package/dist/lib/tokenize.js +48 -0
  131. package/dist/lib/tokenize.js.map +1 -0
  132. package/dist/lib/uiapi.d.ts +109 -0
  133. package/dist/lib/uiapi.js +159 -0
  134. package/dist/lib/uiapi.js.map +1 -0
  135. package/dist/lib/validator.d.ts +27 -0
  136. package/dist/lib/validator.js +100 -0
  137. package/dist/lib/validator.js.map +1 -0
  138. package/dist/lib/variable-promotion.d.ts +69 -0
  139. package/dist/lib/variable-promotion.js +95 -0
  140. package/dist/lib/variable-promotion.js.map +1 -0
  141. package/dist/lib/walker.d.ts +147 -0
  142. package/dist/lib/walker.js +723 -0
  143. package/dist/lib/walker.js.map +1 -0
  144. package/dist/mcp/server.d.ts +7 -0
  145. package/dist/mcp/server.js +34 -0
  146. package/dist/mcp/server.js.map +1 -0
  147. package/dist/mcp/stdio.d.ts +7 -0
  148. package/dist/mcp/stdio.js +25 -0
  149. package/dist/mcp/stdio.js.map +1 -0
  150. package/dist/mcp/tools/echo.d.ts +7 -0
  151. package/dist/mcp/tools/echo.js +17 -0
  152. package/dist/mcp/tools/echo.js.map +1 -0
  153. package/dist/mcp/tools/sf-gql-aggregate.d.ts +11 -0
  154. package/dist/mcp/tools/sf-gql-aggregate.js +75 -0
  155. package/dist/mcp/tools/sf-gql-aggregate.js.map +1 -0
  156. package/dist/mcp/tools/sf-gql-create.d.ts +11 -0
  157. package/dist/mcp/tools/sf-gql-create.js +35 -0
  158. package/dist/mcp/tools/sf-gql-create.js.map +1 -0
  159. package/dist/mcp/tools/sf-gql-delete.d.ts +11 -0
  160. package/dist/mcp/tools/sf-gql-delete.js +31 -0
  161. package/dist/mcp/tools/sf-gql-delete.js.map +1 -0
  162. package/dist/mcp/tools/sf-gql-detail.d.ts +11 -0
  163. package/dist/mcp/tools/sf-gql-detail.js +58 -0
  164. package/dist/mcp/tools/sf-gql-detail.js.map +1 -0
  165. package/dist/mcp/tools/sf-gql-discover.d.ts +9 -0
  166. package/dist/mcp/tools/sf-gql-discover.js +51 -0
  167. package/dist/mcp/tools/sf-gql-discover.js.map +1 -0
  168. package/dist/mcp/tools/sf-gql-list.d.ts +11 -0
  169. package/dist/mcp/tools/sf-gql-list.js +53 -0
  170. package/dist/mcp/tools/sf-gql-list.js.map +1 -0
  171. package/dist/mcp/tools/sf-gql-raw.d.ts +11 -0
  172. package/dist/mcp/tools/sf-gql-raw.js +39 -0
  173. package/dist/mcp/tools/sf-gql-raw.js.map +1 -0
  174. package/dist/mcp/tools/sf-gql-update.d.ts +11 -0
  175. package/dist/mcp/tools/sf-gql-update.js +35 -0
  176. package/dist/mcp/tools/sf-gql-update.js.map +1 -0
  177. package/dist/mcp/tools/shared/zod-schemas.d.ts +49 -0
  178. package/dist/mcp/tools/shared/zod-schemas.js +46 -0
  179. package/dist/mcp/tools/shared/zod-schemas.js.map +1 -0
  180. package/package.json +36 -0
  181. package/ralph-loop.sh +120 -0
  182. package/scripts/smoke-orderby.sh +190 -0
  183. package/src/__tests__/helpers/prime-deps.ts +46 -0
  184. package/src/__tests__/helpers/schema.ts +73 -0
  185. package/src/__tests__/helpers/stdout.ts +33 -0
  186. package/src/__tests__/setup.ts +19 -0
  187. package/src/cli.ts +764 -0
  188. package/src/commands/__tests__/query.spec.ts +137 -0
  189. package/src/commands/args.ts +306 -0
  190. package/src/commands/build.ts +288 -0
  191. package/src/commands/connect.ts +60 -0
  192. package/src/commands/describe.ts +246 -0
  193. package/src/commands/meta.ts +171 -0
  194. package/src/commands/navigate.ts +134 -0
  195. package/src/commands/query-helpers.ts +1202 -0
  196. package/src/commands/query.ts +1085 -0
  197. package/src/commands/review.ts +670 -0
  198. package/src/commands/session-mgmt.ts +256 -0
  199. package/src/commands/type.ts +437 -0
  200. package/src/commands/validate-input.ts +38 -0
  201. package/src/intent/__tests__/build-aggregate.spec.ts +931 -0
  202. package/src/intent/__tests__/build-create-validation.spec.ts +135 -0
  203. package/src/intent/__tests__/build-delete.spec.ts +121 -0
  204. package/src/intent/__tests__/build-detail.spec.ts +333 -0
  205. package/src/intent/__tests__/build-discover.spec.ts +432 -0
  206. package/src/intent/__tests__/build-list.spec.ts +284 -0
  207. package/src/intent/__tests__/build-mutation.spec.ts +108 -0
  208. package/src/intent/__tests__/build-output.spec.ts +55 -0
  209. package/src/intent/__tests__/build-raw.spec.ts +153 -0
  210. package/src/intent/__tests__/build-update-validation.spec.ts +134 -0
  211. package/src/intent/build-aggregate.ts +182 -0
  212. package/src/intent/build-create.ts +19 -0
  213. package/src/intent/build-delete.ts +62 -0
  214. package/src/intent/build-detail.ts +95 -0
  215. package/src/intent/build-discover.ts +199 -0
  216. package/src/intent/build-list.ts +91 -0
  217. package/src/intent/build-mutation.ts +75 -0
  218. package/src/intent/build-output.ts +72 -0
  219. package/src/intent/build-raw.ts +61 -0
  220. package/src/intent/build-update.ts +19 -0
  221. package/src/intent/get-schema-with-priming.ts +43 -0
  222. package/src/intent/select-child-relationship.ts +48 -0
  223. package/src/intent/types.ts +181 -0
  224. package/src/lib/__tests__/apply-command.parity.spec.ts +97 -0
  225. package/src/lib/__tests__/apply-command.spec.ts +171 -0
  226. package/src/lib/__tests__/auth.spec.ts +292 -0
  227. package/src/lib/__tests__/formatter.spec.ts +86 -0
  228. package/src/lib/__tests__/graphql-name.spec.ts +64 -0
  229. package/src/lib/__tests__/introspect.spec.ts +399 -0
  230. package/src/lib/__tests__/object-info.spec.ts +124 -0
  231. package/src/lib/__tests__/path-selection.spec.ts +219 -0
  232. package/src/lib/__tests__/prime-schema.spec.ts +269 -0
  233. package/src/lib/__tests__/query-builder.spec.ts +95 -0
  234. package/src/lib/__tests__/query-commands.spec.ts +74 -0
  235. package/src/lib/__tests__/session.spec.ts +292 -0
  236. package/src/lib/__tests__/tokenize.spec.ts +33 -0
  237. package/src/lib/__tests__/uiapi.spec.ts +192 -0
  238. package/src/lib/__tests__/variable-promotion.spec.ts +211 -0
  239. package/src/lib/__tests__/walker.spec.ts +250 -0
  240. package/src/lib/apply-command.ts +286 -0
  241. package/src/lib/auth.ts +157 -0
  242. package/src/lib/codegen.ts +899 -0
  243. package/src/lib/command-registry.ts +434 -0
  244. package/src/lib/formatter.ts +587 -0
  245. package/src/lib/fs-utils.ts +46 -0
  246. package/src/lib/graphql-name.ts +35 -0
  247. package/src/lib/interactive.ts +634 -0
  248. package/src/lib/introspect.ts +320 -0
  249. package/src/lib/object-info.ts +409 -0
  250. package/src/lib/path-selection.ts +162 -0
  251. package/src/lib/prime-schema.ts +195 -0
  252. package/src/lib/query-builder.ts +193 -0
  253. package/src/lib/query-commands.ts +290 -0
  254. package/src/lib/session.ts +1304 -0
  255. package/src/lib/tokenize.ts +43 -0
  256. package/src/lib/uiapi.ts +176 -0
  257. package/src/lib/validator.ts +143 -0
  258. package/src/lib/variable-promotion.ts +145 -0
  259. package/src/lib/walker.ts +975 -0
  260. package/src/mcp/__tests__/server.spec.ts +155 -0
  261. package/src/mcp/server.ts +38 -0
  262. package/src/mcp/stdio.ts +29 -0
  263. package/src/mcp/tools/__tests__/sf-gql-aggregate.spec.ts +173 -0
  264. package/src/mcp/tools/__tests__/sf-gql-create.spec.ts +235 -0
  265. package/src/mcp/tools/__tests__/sf-gql-delete.spec.ts +194 -0
  266. package/src/mcp/tools/__tests__/sf-gql-detail.spec.ts +246 -0
  267. package/src/mcp/tools/__tests__/sf-gql-discover.spec.ts +320 -0
  268. package/src/mcp/tools/__tests__/sf-gql-list.spec.ts +128 -0
  269. package/src/mcp/tools/__tests__/sf-gql-raw.spec.ts +141 -0
  270. package/src/mcp/tools/__tests__/sf-gql-update.spec.ts +207 -0
  271. package/src/mcp/tools/echo.ts +24 -0
  272. package/src/mcp/tools/sf-gql-aggregate.ts +102 -0
  273. package/src/mcp/tools/sf-gql-create.ts +55 -0
  274. package/src/mcp/tools/sf-gql-delete.ts +49 -0
  275. package/src/mcp/tools/sf-gql-detail.ts +85 -0
  276. package/src/mcp/tools/sf-gql-discover.ts +67 -0
  277. package/src/mcp/tools/sf-gql-list.ts +73 -0
  278. package/src/mcp/tools/sf-gql-raw.ts +56 -0
  279. package/src/mcp/tools/sf-gql-update.ts +55 -0
  280. package/src/mcp/tools/shared/zod-schemas.ts +55 -0
  281. package/tsconfig.json +18 -0
  282. package/vitest.config.ts +14 -0
@@ -0,0 +1,1085 @@
1
+ /**
2
+ * Copyright (c) 2026, Salesforce, Inc.,
3
+ * All rights reserved.
4
+ * For full license text, see the LICENSE.txt file
5
+ */
6
+ /* eslint-disable @typescript-eslint/no-explicit-any -- graphiti traverses untyped schema/introspection JSON; see follow-up to replace with `unknown` + narrowing */
7
+
8
+ /**
9
+ * Barrel module — re-exports all query subcommands so existing imports
10
+ * like `from '../commands/query.js'` continue to work unchanged.
11
+ */
12
+
13
+ // ── Re-exports from query-helpers ─────────────────────────────────────────────
14
+ export {
15
+ CommandError,
16
+ EXIT_CODES,
17
+ getSessionSchema,
18
+ walkerResultAtPath,
19
+ parsePathInput,
20
+ resolveAliasSegments,
21
+ resolveDirectoryPathInSession,
22
+ resolveArgsPath,
23
+ resolveVariablesPath,
24
+ getCurrentInstances,
25
+ getActiveInstanceId,
26
+ buildSelectionInfo,
27
+ buildFieldLongInfo,
28
+ formatAliasContext,
29
+ detectSObjectName,
30
+ printDirectory,
31
+ printArgsDirectory,
32
+ printVariablesDirectory,
33
+ printQuery,
34
+ requireFieldDirectory,
35
+ dotPathToSlashPath,
36
+ selectLeafInSession,
37
+ pathPointsToArgs,
38
+ resolveArgType,
39
+ validateVariableAssignment,
40
+ validateLiteralAssignment,
41
+ assignViaPath,
42
+ emitObjectInfoWarnings,
43
+ assignInArgsContext,
44
+ assignInVariablesContext,
45
+ inferTypeFromArgsPath,
46
+ } from "./query-helpers.js";
47
+ export type { WalkerResult } from "./query-helpers.js";
48
+
49
+ // ── Re-exports from navigate ──────────────────────────────────────────────────
50
+ export { queryPwd, queryLs, queryCd } from "./navigate.js";
51
+
52
+ // ── Re-exports from build ─────────────────────────────────────────────────────
53
+ export { querySelect, querySelectLs, queryRm, queryMkdir } from "./build.js";
54
+
55
+ // ── Re-exports from args ──────────────────────────────────────────────────────
56
+ export { queryAssign, queryUnassign, queryDefine } from "./args.js";
57
+
58
+ // ── Re-exports from review ────────────────────────────────────────────────────
59
+ export {
60
+ queryShow,
61
+ queryShowJson,
62
+ queryShowQueryOnly,
63
+ queryValidate,
64
+ queryValidateAll,
65
+ queryExecute,
66
+ queryCodegen,
67
+ buildSampleInputFields,
68
+ validateStrictMutation,
69
+ } from "./review.js";
70
+
71
+ // ── Re-exports from meta ──────────────────────────────────────────────────────
72
+ export { queryExample, printInputExamples, queryHelp } from "./meta.js";
73
+
74
+ // ── Re-exports from session-mgmt ──────────────────────────────────────────────
75
+ export {
76
+ queryNew,
77
+ queryClone,
78
+ querySessionsList,
79
+ querySessionsRm,
80
+ querySessionsPrune,
81
+ querySessionsClean,
82
+ formatAge,
83
+ parseDuration,
84
+ } from "./session-mgmt.js";
85
+
86
+ // ── Imports used by the dispatcher ────────────────────────────────────────────
87
+ import { queryAssign, queryUnassign, queryDefine } from "./args.js";
88
+ import { querySelectLs, queryRm, queryMkdir } from "./build.js";
89
+ import { queryHelp } from "./meta.js";
90
+ import { queryPwd, queryLs, queryCd } from "./navigate.js";
91
+ import {
92
+ printQuery,
93
+ selectLeafInSession,
94
+ getSessionSchema,
95
+ buildSelectionInfo,
96
+ buildFieldLongInfo,
97
+ walkerResultAtPath,
98
+ } from "./query-helpers.js";
99
+ import { CommandError } from "./query-helpers.js";
100
+ import {
101
+ queryShow,
102
+ queryShowJson,
103
+ queryShowQueryOnly,
104
+ queryValidate,
105
+ queryValidateAll,
106
+ queryExecute,
107
+ queryCodegen,
108
+ } from "./review.js";
109
+ import {
110
+ queryClone,
111
+ querySessionsList,
112
+ querySessionsRm,
113
+ querySessionsPrune,
114
+ querySessionsClean,
115
+ } from "./session-mgmt.js";
116
+ import { getOutputMode } from "../lib/command-registry.js";
117
+ import { renderQuery } from "../lib/query-builder.js";
118
+ import { formatHelp, isInteractiveMode } from "../lib/query-commands.js";
119
+ import {
120
+ loadSession,
121
+ saveSession,
122
+ pushUndoSnapshot,
123
+ popUndo,
124
+ formatPath,
125
+ getNavigationContext,
126
+ isInArgsContext,
127
+ queryNavToSchemaPath,
128
+ getChildren,
129
+ type FieldProjectionNode,
130
+ } from "../lib/session.js";
131
+ import { validateQuery } from "../lib/validator.js";
132
+ import { getRootFields } from "../lib/walker.js";
133
+
134
+ interface ParsedSetArgs {
135
+ specs: { path: string; value: string }[];
136
+ deferredVarDefs: [string, string][];
137
+ cursorFieldPrefix: string;
138
+ hasCursor: boolean;
139
+ }
140
+
141
+ /**
142
+ * Parse inline key=value args with full shorthand support:
143
+ * - where.Field=Value → where={"Field":{"eq":"Value"}}
144
+ * - where.Field=$var → deferred variable binding
145
+ * - orderBy=Field:DESC → orderBy.Field.order=DESC
146
+ * - cursor → deferred $after variable definition
147
+ */
148
+ function parseInlineSetArgs(tokens: string[]): ParsedSetArgs {
149
+ const specs: { path: string; value: string }[] = [];
150
+ const deferredVarDefs: [string, string][] = [];
151
+
152
+ // Normalize: split any space-containing tokens (e.g. from quoted shell args)
153
+ const expanded: string[] = [];
154
+ for (const t of tokens) {
155
+ if (t.includes(" ") && !t.startsWith("{") && !t.startsWith("[")) {
156
+ expanded.push(...t.split(/\s+/));
157
+ } else {
158
+ expanded.push(t);
159
+ }
160
+ }
161
+
162
+ const hasCursor = expanded.includes("cursor");
163
+ const filteredNoCursor = hasCursor ? expanded.filter((t) => t !== "cursor") : expanded;
164
+
165
+ let fieldPrefix = "";
166
+ let startIdx = 0;
167
+
168
+ if (filteredNoCursor.length > 0 && !filteredNoCursor[0].includes("=")) {
169
+ fieldPrefix = filteredNoCursor[0];
170
+ startIdx = 1;
171
+ }
172
+
173
+ for (let i = startIdx; i < filteredNoCursor.length; i++) {
174
+ const token = filteredNoCursor[i];
175
+ const eqIdx = token.indexOf("=");
176
+ if (eqIdx > 0) {
177
+ let key = token.slice(0, eqIdx);
178
+ let val = token.slice(eqIdx + 1);
179
+
180
+ // Collect continuation tokens for multi-word where values (e.g. where.Status=On Hold)
181
+ const isWhereShorthand = /^(.*?)?where\.(\w+)(?:\.(\w+))?$/.test(key);
182
+ if (isWhereShorthand && !val.startsWith("$") && !val.startsWith("{")) {
183
+ while (i + 1 < filteredNoCursor.length) {
184
+ const next = filteredNoCursor[i + 1];
185
+ if (next.includes("=") || next === "cursor" || next.startsWith("--")) break;
186
+ val += " " + next;
187
+ i++;
188
+ }
189
+ }
190
+
191
+ // orderBy shorthand: orderBy=Field:DESC → orderBy.Field.order=DESC
192
+ if (key === "orderBy" || key.endsWith("/orderBy") || key.endsWith(".orderBy")) {
193
+ const orderMatch = val.match(/^(\w+):(ASC|DESC)$/i);
194
+ if (orderMatch) {
195
+ key = `${key}.${orderMatch[1]}.order`;
196
+ val = orderMatch[2].toUpperCase();
197
+ }
198
+ }
199
+
200
+ // where shorthand: where.Field=Value → where={"Field":{"eq":"Value"}}
201
+ const whereMatch = key.match(/^(.*?)?where\.(\w+)(?:\.(\w+))?$/);
202
+ if (whereMatch) {
203
+ const prefix = whereMatch[1] || "";
204
+ const field = whereMatch[2];
205
+ // Auto-detect `like` operator when value contains SQL wildcards (%) and no explicit operator
206
+ const op =
207
+ whereMatch[3] ||
208
+ (val.includes("%") && !val.startsWith("$") && !val.startsWith("{") ? "like" : "eq");
209
+
210
+ // Warn on empty where value — likely shell expanded $var to empty string
211
+ if (val === "" && !isInteractiveMode()) {
212
+ const hint = `Warning: ${key}= has an empty value. Did the shell expand a $variable?\n Tip: use single quotes to prevent expansion: '${key}=$yourVar'`;
213
+ console.error(hint);
214
+ }
215
+
216
+ if (val.startsWith("$")) {
217
+ let resolvedPrefix = fieldPrefix;
218
+ if (
219
+ resolvedPrefix &&
220
+ !resolvedPrefix.startsWith("/") &&
221
+ !resolvedPrefix.startsWith("query/") &&
222
+ !resolvedPrefix.startsWith("@args")
223
+ ) {
224
+ resolvedPrefix = `query/${resolvedPrefix}`;
225
+ }
226
+ const varPath = resolvedPrefix
227
+ ? `${resolvedPrefix}/@args/${prefix}where/${field}/${op}`
228
+ : `${prefix}where/${field}/${op}`;
229
+ deferredVarDefs.push([val, varPath]);
230
+ continue;
231
+ }
232
+
233
+ key = `${prefix}where`;
234
+ let parsedVal: unknown = val;
235
+ if (val === "null") parsedVal = null;
236
+ else if (val === "true") parsedVal = true;
237
+ else if (val === "false") parsedVal = false;
238
+ else if (/^-?\d+(\.\d+)?$/.test(val)) parsedVal = Number(val);
239
+ else parsedVal = val;
240
+ val = JSON.stringify({ [field]: { [op]: parsedVal } });
241
+ }
242
+
243
+ let resolvedPrefix = fieldPrefix;
244
+ if (
245
+ resolvedPrefix &&
246
+ !resolvedPrefix.startsWith("/") &&
247
+ !resolvedPrefix.startsWith("query/") &&
248
+ !resolvedPrefix.startsWith("@args")
249
+ ) {
250
+ resolvedPrefix = `query/${resolvedPrefix}`;
251
+ }
252
+ const fullPath = resolvedPrefix ? `${resolvedPrefix}/@args/${key}` : key;
253
+ specs.push({ path: fullPath, value: val });
254
+ }
255
+ }
256
+ return { specs, deferredVarDefs, cursorFieldPrefix: fieldPrefix, hasCursor };
257
+ }
258
+
259
+ /**
260
+ * Builds a JSON result object for commands that don't have their own JSON handler.
261
+ * Called after the command executes to capture the result state.
262
+ */
263
+ function buildJsonResult(
264
+ command: string,
265
+ sessionId: string,
266
+ extra: Record<string, unknown> = {},
267
+ ): Record<string, unknown> {
268
+ try {
269
+ const session = loadSession(sessionId);
270
+ const result: Record<string, unknown> = { command, ...extra };
271
+
272
+ switch (command) {
273
+ case "pwd":
274
+ result.path = formatPath(session.navigationPath);
275
+ break;
276
+
277
+ case "cd":
278
+ result.path = formatPath(session.navigationPath);
279
+ break;
280
+
281
+ case "ls": {
282
+ result.path = formatPath(session.navigationPath);
283
+ const ctx = getNavigationContext(session.navigationPath);
284
+ if (ctx === "query" && !isInArgsContext(session.navigationPath)) {
285
+ const schemaPath = queryNavToSchemaPath(session.navigationPath);
286
+ if (schemaPath.length > 0) {
287
+ try {
288
+ const schema = getSessionSchema(session);
289
+ const _wr = getRootFields(schema, session.operation);
290
+ const walkerRes = walkerResultAtPath(session);
291
+ const { selectedFields } = buildSelectionInfo(session);
292
+ const fieldLongInfo = buildFieldLongInfo(session, walkerRes);
293
+ result.type = { name: walkerRes.typeName, kind: walkerRes.kind };
294
+ result.hasArgs = walkerRes.args.length > 0;
295
+ result.args = walkerRes.args.map((a) => ({
296
+ name: a.name,
297
+ type: a.typeName,
298
+ required: a.isNonNull,
299
+ }));
300
+ result.fields = walkerRes.fields.map((f) => {
301
+ const entry: Record<string, unknown> = {
302
+ name: f.name,
303
+ type: f.typeName,
304
+ kind: f.typeKind,
305
+ selected: selectedFields.has(f.name),
306
+ };
307
+ const longInfo = fieldLongInfo.get(f.name);
308
+ if (longInfo) entry.metadata = longInfo;
309
+ return entry;
310
+ });
311
+ result.totalFields = walkerRes.fields.length;
312
+ } catch {
313
+ /* path may not resolve */
314
+ }
315
+ }
316
+ }
317
+ break;
318
+ }
319
+
320
+ case "select":
321
+ case "drop":
322
+ case "alias":
323
+ case "set":
324
+ case "unset":
325
+ case "var":
326
+ case "optional":
327
+ case "reset":
328
+ case "undo":
329
+ case "clone":
330
+ result.query = renderQuery(session);
331
+ result.path = formatPath(session.navigationPath);
332
+ result.selectedFields = session.nodes.filter(
333
+ (n) => n.kind === "field" && getChildren(session, n.id).length === 0,
334
+ ).length;
335
+ result.totalNodes = session.nodes.length;
336
+ if (session.variables.length > 0) {
337
+ result.variables = session.variables.map((v) => ({
338
+ name: v.name,
339
+ type: v.type,
340
+ defaultValue: v.defaultValue ?? null,
341
+ runtimeValue: v.runtimeValue ?? null,
342
+ }));
343
+ }
344
+ break;
345
+
346
+ case "check": {
347
+ const schema = getSessionSchema(session);
348
+ const queryString = renderQuery(session);
349
+ const errors = validateQuery(schema, queryString);
350
+ result.valid = errors.length === 0;
351
+ result.query = queryString;
352
+ if (errors.length > 0) {
353
+ result.errors = errors.map((e) => ({
354
+ message: typeof e === "string" ? e : ((e as any).message ?? String(e)),
355
+ }));
356
+ }
357
+ break;
358
+ }
359
+
360
+ default:
361
+ break;
362
+ }
363
+
364
+ return result;
365
+ } catch {
366
+ return { command, ...extra };
367
+ }
368
+ }
369
+
370
+ // ── Shared dispatcher ─────────────────────────────────────────────────────────
371
+
372
+ export async function dispatchQueryCommand(
373
+ sessionId: string,
374
+ subcommand: string | undefined,
375
+ rawRest: string[],
376
+ opts: {
377
+ search?: string;
378
+ regex?: string;
379
+ long?: boolean;
380
+ all?: boolean;
381
+ as?: string;
382
+ quiet?: boolean;
383
+ dataCloud?: boolean;
384
+ } = {},
385
+ ): Promise<Record<string, unknown> | void> {
386
+ const jsonMode = rawRest.includes("--json") || getOutputMode() === "json";
387
+
388
+ // Extract standard display options from rawRest (needed for chain mode where
389
+ // opts is empty and flags like --search, -l, -a arrive as raw tokens).
390
+ const rest: string[] = [];
391
+ for (let i = 0; i < rawRest.length; i++) {
392
+ const tok = rawRest[i];
393
+ if (tok === "--json") continue;
394
+ if (tok === "--search" && i + 1 < rawRest.length) {
395
+ opts.search = opts.search ?? rawRest[++i];
396
+ } else if (tok === "--regex" && i + 1 < rawRest.length) {
397
+ opts.regex = opts.regex ?? rawRest[++i];
398
+ } else if (tok === "--as" && i + 1 < rawRest.length) {
399
+ opts.as = opts.as ?? rawRest[++i];
400
+ } else if (tok === "-l" || tok === "--long") {
401
+ opts.long = true;
402
+ } else if (tok === "-a" || tok === "--all") {
403
+ opts.all = true;
404
+ } else if (tok === "-q" || tok === "--quiet") {
405
+ opts.quiet = true;
406
+ } else if (tok === "--data-cloud") {
407
+ opts.dataCloud = true;
408
+ } else {
409
+ rest.push(tok);
410
+ }
411
+ }
412
+
413
+ if (!subcommand) {
414
+ if (jsonMode) {
415
+ queryShowJson(sessionId);
416
+ return;
417
+ }
418
+ queryShow(sessionId);
419
+ return;
420
+ }
421
+
422
+ // Suppress human output in JSON mode by temporarily redirecting console.log
423
+ const origLog = console.log;
424
+ const origError = console.error;
425
+ const capturedOutput: string[] = [];
426
+ if (jsonMode && subcommand !== "show" && subcommand !== "run" && subcommand !== "codegen") {
427
+ console.log = (...args: unknown[]) => {
428
+ capturedOutput.push(args.map(String).join(" "));
429
+ };
430
+ console.error = (...args: unknown[]) => {
431
+ capturedOutput.push(args.map(String).join(" "));
432
+ };
433
+ }
434
+
435
+ try {
436
+ if (!isInteractiveMode() && !opts.quiet && !jsonMode) {
437
+ const echoArgs = [...rest];
438
+ if (opts.long) echoArgs.push("-l");
439
+ if (opts.all) echoArgs.push("-a");
440
+ if (opts.search) echoArgs.push(`--search ${opts.search}`);
441
+ if (opts.regex) echoArgs.push(`--regex ${opts.regex}`);
442
+ if (opts.as) echoArgs.push(`--as ${opts.as}`);
443
+ const echoLine =
444
+ echoArgs.length > 0 ? `> ${subcommand} ${echoArgs.join(" ")}` : `> ${subcommand}`;
445
+ origLog(echoLine);
446
+ }
447
+
448
+ const MUTATING_COMMANDS = new Set([
449
+ "cd",
450
+ "select",
451
+ "drop",
452
+ "set",
453
+ "unset",
454
+ "alias",
455
+ "var",
456
+ "optional",
457
+ "reset",
458
+ ]);
459
+ if (subcommand && MUTATING_COMMANDS.has(subcommand)) {
460
+ const session = loadSession(sessionId);
461
+ pushUndoSnapshot(session);
462
+ saveSession(session);
463
+ }
464
+
465
+ switch (subcommand) {
466
+ case "pwd": {
467
+ queryPwd(sessionId);
468
+ break;
469
+ }
470
+
471
+ case "cd": {
472
+ const target = rest[0] ?? "/";
473
+ queryCd(sessionId, target);
474
+ break;
475
+ }
476
+
477
+ case "select": {
478
+ if (rest[0] === "ls") {
479
+ querySelectLs(sessionId);
480
+ break;
481
+ }
482
+ const applyOptional = rest.includes("--optional");
483
+ const selectItems: { spec: string; alias?: string }[] = [];
484
+ for (const rawToken of rest) {
485
+ if (rawToken.startsWith("--")) continue;
486
+ const subTokens = rawToken.includes(" ")
487
+ ? rawToken.split(/\s+/).filter(Boolean)
488
+ : [rawToken];
489
+ for (const token of subTokens) {
490
+ const colonIdx = token.lastIndexOf(":");
491
+ const afterColon = colonIdx > 0 ? token.slice(colonIdx + 1) : "";
492
+ if (colonIdx > 0 && afterColon.length > 0 && !/[./:]/.test(afterColon)) {
493
+ selectItems.push({ spec: token.slice(0, colonIdx), alias: afterColon });
494
+ } else {
495
+ selectItems.push({ spec: token });
496
+ }
497
+ }
498
+ }
499
+ if (selectItems.length === 1 && !selectItems[0].alias && opts.as) {
500
+ selectItems[0].alias = opts.as;
501
+ }
502
+ if (selectItems.length === 0) {
503
+ console.error(
504
+ "Usage: select <path[:alias]> [<path[:alias]> ...] or select <leaf> --as <alias>",
505
+ );
506
+ return;
507
+ }
508
+ const session = loadSession(sessionId);
509
+ const successMessages: string[] = [];
510
+ const failureMessages: string[] = [];
511
+ for (const item of selectItems) {
512
+ const snapNodes = JSON.parse(JSON.stringify(session.nodes));
513
+ const snapFocus = JSON.parse(JSON.stringify(session.focusByPath));
514
+ const nodeCountBefore = session.nodes.length;
515
+ try {
516
+ selectLeafInSession(session, item.spec, item.alias);
517
+ if (applyOptional) {
518
+ for (let ni = nodeCountBefore; ni < session.nodes.length; ni++) {
519
+ const node = session.nodes[ni];
520
+ if (!node.directives.some((d) => d.name === "optional")) {
521
+ node.directives.push({ name: "optional", args: {} });
522
+ }
523
+ }
524
+ }
525
+ const optTag = applyOptional ? " @optional" : "";
526
+ successMessages.push(
527
+ `Selected ${item.spec}${item.alias ? ` as ${item.alias}` : ""}${optTag}.`,
528
+ );
529
+ } catch (error: any) {
530
+ session.nodes = snapNodes;
531
+ session.focusByPath = snapFocus;
532
+ failureMessages.push(`Failed: ${item.spec} — ${error.message}`);
533
+ }
534
+ }
535
+ if (successMessages.length > 0) {
536
+ saveSession(session);
537
+ }
538
+ if (!opts.quiet) {
539
+ for (const msg of successMessages) console.log(msg);
540
+ if (failureMessages.length > 0) {
541
+ console.log("");
542
+ for (const msg of failureMessages) console.error(msg);
543
+ }
544
+ if (successMessages.length > 0) {
545
+ console.log("");
546
+ printQuery(session);
547
+ }
548
+ }
549
+ if (failureMessages.length > 0 && successMessages.length === 0) {
550
+ throw new Error(failureMessages.join("\n"));
551
+ }
552
+ break;
553
+ }
554
+
555
+ case "ls": {
556
+ const lsDirs = rest.filter((r) => !r.startsWith("-"));
557
+ await queryLs(
558
+ sessionId,
559
+ {
560
+ search: opts.search,
561
+ regex: opts.regex,
562
+ long: opts.long,
563
+ all: opts.all,
564
+ dataCloud: opts.dataCloud,
565
+ },
566
+ lsDirs.length > 0 ? lsDirs : undefined,
567
+ );
568
+ break;
569
+ }
570
+
571
+ case "set": {
572
+ const isDefault = rest.includes("--default");
573
+ const filtered = rest.filter((r) => r !== "--default" && !r.startsWith("--"));
574
+
575
+ // cursor shorthand: `set uiapi/query/Case cursor` or mixed with other args
576
+ // Extract 'cursor' token and process it after the other set operations
577
+ const hasCursor = filtered.includes("cursor");
578
+ const filteredNoCursor = hasCursor ? filtered.filter((t) => t !== "cursor") : filtered;
579
+
580
+ const specs: { path: string; value: string }[] = [];
581
+
582
+ const hasKeyValue = filteredNoCursor.some(
583
+ (t) => t.includes("=") && !t.startsWith("$") && !t.startsWith("{") && !t.startsWith("["),
584
+ );
585
+
586
+ // Determine field prefix (first non-key=value token)
587
+ let cursorFieldPrefix = "";
588
+
589
+ // Deferred variable definitions from where.Field=$var shorthand
590
+ const deferredVarDefs: [string, string][] = [];
591
+
592
+ if (hasKeyValue) {
593
+ let fieldPrefix = "";
594
+ let startIdx = 0;
595
+
596
+ if (filteredNoCursor.length > 0 && !filteredNoCursor[0].includes("=")) {
597
+ fieldPrefix = filteredNoCursor[0];
598
+ startIdx = 1;
599
+ }
600
+ cursorFieldPrefix = fieldPrefix;
601
+
602
+ for (let i = startIdx; i < filteredNoCursor.length; i++) {
603
+ const token = filteredNoCursor[i];
604
+ const eqIdx = token.indexOf("=");
605
+ if (eqIdx > 0) {
606
+ let key = token.slice(0, eqIdx);
607
+ let val = token.slice(eqIdx + 1);
608
+
609
+ // orderBy shorthand: orderBy=Field:DESC → orderBy.Field.order=DESC
610
+ if (key === "orderBy" || key.endsWith("/orderBy") || key.endsWith(".orderBy")) {
611
+ const orderMatch = val.match(/^(\w+):(ASC|DESC)$/i);
612
+ if (orderMatch) {
613
+ key = `${key}.${orderMatch[1]}.order`;
614
+ val = orderMatch[2].toUpperCase();
615
+ }
616
+ }
617
+
618
+ // where shorthand: where.Field=Value → where={"Field":{"eq":"Value"}}
619
+ // Also supports: where.Field.op=Value (e.g. where.Name.like=%test%)
620
+ // Variable binding: where.Field=$var → defines $var at where/Field/eq
621
+ const whereMatch = key.match(/^(.*?)?where\.(\w+)(?:\.(\w+))?$/);
622
+ if (whereMatch) {
623
+ const prefix = whereMatch[1] || "";
624
+ const field = whereMatch[2];
625
+ // Auto-detect `like` operator when value contains SQL wildcards (%) and no explicit operator
626
+ const op =
627
+ whereMatch[3] ||
628
+ (val.includes("%") && !val.startsWith("$") && !val.startsWith("{")
629
+ ? "like"
630
+ : "eq");
631
+
632
+ // If value starts with $, treat as variable binding
633
+ if (val.startsWith("$")) {
634
+ let resolvedPrefix = fieldPrefix;
635
+ if (
636
+ resolvedPrefix &&
637
+ !resolvedPrefix.startsWith("/") &&
638
+ !resolvedPrefix.startsWith("query/") &&
639
+ !resolvedPrefix.startsWith("@args")
640
+ ) {
641
+ resolvedPrefix = `query/${resolvedPrefix}`;
642
+ }
643
+ const varPath = resolvedPrefix
644
+ ? `${resolvedPrefix}/@args/${prefix}where/${field}/${op}`
645
+ : `${prefix}where/${field}/${op}`;
646
+ deferredVarDefs.push([val, varPath]);
647
+ continue;
648
+ }
649
+
650
+ key = `${prefix}where`;
651
+ // Try to parse as number/boolean/null, otherwise use string
652
+ let parsedVal: unknown = val;
653
+ if (val === "null") parsedVal = null;
654
+ else if (val === "true") parsedVal = true;
655
+ else if (val === "false") parsedVal = false;
656
+ else if (/^-?\d+(\.\d+)?$/.test(val)) parsedVal = Number(val);
657
+ else parsedVal = val;
658
+ val = JSON.stringify({ [field]: { [op]: parsedVal } });
659
+ }
660
+
661
+ let resolvedPrefix = fieldPrefix;
662
+ if (
663
+ resolvedPrefix &&
664
+ !resolvedPrefix.startsWith("/") &&
665
+ !resolvedPrefix.startsWith("query/") &&
666
+ !resolvedPrefix.startsWith("@args")
667
+ ) {
668
+ resolvedPrefix = `query/${resolvedPrefix}`;
669
+ }
670
+ const fullPath = resolvedPrefix ? `${resolvedPrefix}/@args/${key}` : key;
671
+ specs.push({ path: fullPath, value: val });
672
+ } else {
673
+ if (i + 1 < filteredNoCursor.length) {
674
+ let resolvedPrefix = fieldPrefix;
675
+ if (
676
+ resolvedPrefix &&
677
+ !resolvedPrefix.startsWith("/") &&
678
+ !resolvedPrefix.startsWith("query/") &&
679
+ !resolvedPrefix.startsWith("@args")
680
+ ) {
681
+ resolvedPrefix = `query/${resolvedPrefix}`;
682
+ }
683
+ const path = resolvedPrefix ? `${resolvedPrefix}/@args/${token}` : token;
684
+ specs.push({ path, value: filteredNoCursor[i + 1] });
685
+ i++;
686
+ }
687
+ }
688
+ }
689
+ } else if (filteredNoCursor.length > 0) {
690
+ for (let i = 0; i < filteredNoCursor.length; i += 2) {
691
+ const dotPath = filteredNoCursor[i];
692
+ const value = filteredNoCursor[i + 1];
693
+ if (!dotPath || value === undefined) {
694
+ console.error("Usage: set [<field-path>] <key>=<value> ... | set <path> <value>");
695
+ return;
696
+ }
697
+ specs.push({ path: dotPath, value });
698
+ }
699
+ }
700
+
701
+ if (specs.length === 0 && !hasCursor && deferredVarDefs.length === 0) {
702
+ console.error("Usage: set [<field-path>] <key>=<value> ... | set <path> <value>");
703
+ return;
704
+ }
705
+ if (specs.length > 0) {
706
+ queryAssign(sessionId, specs, isDefault);
707
+ }
708
+
709
+ // Process deferred variable definitions from where.Field=$var shorthand
710
+ for (const [varName, varPath] of deferredVarDefs) {
711
+ queryDefine(sessionId, [varName, varPath]);
712
+ }
713
+
714
+ // cursor shorthand: defines $after variable at the after arg of the field prefix
715
+ // and auto-selects pageInfo { hasNextPage, endCursor } for pagination support
716
+ if (hasCursor) {
717
+ // Determine the field prefix for cursor — use the first non-key=value token
718
+ const prefix = cursorFieldPrefix || filteredNoCursor[0] || "";
719
+ if (!prefix) {
720
+ console.error(
721
+ "cursor shorthand requires a field path prefix, e.g. `set uiapi/query/Case cursor`",
722
+ );
723
+ return;
724
+ }
725
+ const cursorPath = `${prefix}/@args/after`;
726
+ queryDefine(sessionId, ["$after", cursorPath]);
727
+
728
+ // Auto-select pageInfo fields needed for pagination loops
729
+ const session = loadSession(sessionId);
730
+ const pageInfoHasNext = `${prefix}/pageInfo/hasNextPage`;
731
+ const pageInfoEndCursor = `${prefix}/pageInfo/endCursor`;
732
+ try {
733
+ selectLeafInSession(session, pageInfoHasNext);
734
+ selectLeafInSession(session, pageInfoEndCursor);
735
+ saveSession(session);
736
+ } catch {
737
+ // pageInfo may already be selected or path may not resolve — ignore
738
+ }
739
+ }
740
+ break;
741
+ }
742
+
743
+ case "unset": {
744
+ const target = rest[0];
745
+ if (!target) {
746
+ console.error("Usage: unset <path> (e.g. unset first, unset where/Name/like)");
747
+ return;
748
+ }
749
+ queryUnassign(sessionId, target);
750
+ break;
751
+ }
752
+
753
+ case "alias": {
754
+ queryMkdir(sessionId, rest);
755
+ break;
756
+ }
757
+
758
+ case "var": {
759
+ if (rest.length === 0) {
760
+ console.error("Usage: var $name [<path> | <Type>] [default]");
761
+ return;
762
+ }
763
+ queryDefine(sessionId, rest);
764
+ break;
765
+ }
766
+
767
+ case "show": {
768
+ if (rest.includes("--query-only") || rest.includes("--raw")) {
769
+ queryShowQueryOnly(sessionId);
770
+ } else if (jsonMode) {
771
+ queryShowJson(sessionId);
772
+ } else {
773
+ queryShow(sessionId);
774
+ }
775
+ break;
776
+ }
777
+
778
+ case "check": {
779
+ if (rest.includes("--all") || opts.all) {
780
+ queryValidateAll();
781
+ break;
782
+ }
783
+ const strict = rest.includes("--strict");
784
+ const withCodegen = rest.includes("--codegen");
785
+ await queryValidate(sessionId, strict, { codegen: withCodegen });
786
+ break;
787
+ }
788
+
789
+ case "run": {
790
+ const adHoc: Record<string, string> = {};
791
+ let dryRun = false;
792
+ for (let i = 0; i < rest.length; i++) {
793
+ if (rest[i] === "--dry-run") {
794
+ dryRun = true;
795
+ continue;
796
+ }
797
+ if (rest[i] === "--var" && rest[i + 1]) {
798
+ const token = rest[++i];
799
+ const eq = token.indexOf("=");
800
+ if (eq !== -1) {
801
+ adHoc[token.slice(0, eq).replace(/^\$/, "")] = token.slice(eq + 1);
802
+ } else if (rest[i + 1] !== undefined && !rest[i + 1].startsWith("--")) {
803
+ adHoc[token.replace(/^\$/, "")] = rest[++i];
804
+ }
805
+ }
806
+ }
807
+ await queryExecute(sessionId, Object.keys(adHoc).length > 0 ? adHoc : undefined, dryRun);
808
+ if (!dryRun) {
809
+ console.log("");
810
+ console.log("Tip: Run `show` for a full session snapshot.");
811
+ }
812
+ break;
813
+ }
814
+
815
+ case "drop": {
816
+ const fieldOrAlias = rest[0];
817
+ if (!fieldOrAlias) {
818
+ console.error("Usage: drop <target> (field, alias, arg, variable, or list index)");
819
+ return;
820
+ }
821
+ queryRm(sessionId, fieldOrAlias);
822
+ break;
823
+ }
824
+
825
+ case "help": {
826
+ const topic = rest[0];
827
+ queryHelp(sessionId, topic);
828
+ break;
829
+ }
830
+
831
+ case "sessions": {
832
+ const sessionsAction = rest[0];
833
+ if (sessionsAction === "rm" || sessionsAction === "delete" || sessionsAction === "remove") {
834
+ const target = rest[1];
835
+ if (!target) {
836
+ console.error("Usage: sessions rm <session-id-or-name>");
837
+ return;
838
+ }
839
+ querySessionsRm(target);
840
+ } else if (sessionsAction === "prune") {
841
+ const olderThanIdx = rest.indexOf("--older-than");
842
+ const duration = olderThanIdx !== -1 ? rest[olderThanIdx + 1] : undefined;
843
+ if (!duration) {
844
+ console.error("Usage: sessions prune --older-than <duration> (e.g. 1d, 12h, 30m)");
845
+ return;
846
+ }
847
+ querySessionsPrune(duration);
848
+ } else if (sessionsAction === "clean") {
849
+ querySessionsClean();
850
+ } else if (sessionsAction) {
851
+ console.error(
852
+ `Unknown sessions action "${sessionsAction}". Use "rm", "prune", or "clean".`,
853
+ );
854
+ } else {
855
+ querySessionsList();
856
+ }
857
+ break;
858
+ }
859
+
860
+ case "reset": {
861
+ const session = loadSession(sessionId);
862
+ session.nodes = [];
863
+ session.variables = [];
864
+ session.focusByPath = {};
865
+ session.navigationPath = [];
866
+ saveSession(session);
867
+ console.log("Session reset. All selections, arguments, and variables cleared.");
868
+ break;
869
+ }
870
+
871
+ case "undo": {
872
+ const session = loadSession(sessionId);
873
+ const restored = popUndo(session);
874
+ if (!restored) {
875
+ console.log("Nothing to undo.");
876
+ break;
877
+ }
878
+
879
+ const diffs: string[] = [];
880
+ if (formatPath(session.navigationPath) !== formatPath(restored.navigationPath)) {
881
+ diffs.push(`navigation → ${formatPath(restored.navigationPath)}`);
882
+ }
883
+ const nodeDelta = session.nodes.length - restored.nodes.length;
884
+ if (nodeDelta > 0) diffs.push(`${nodeDelta} node${nodeDelta !== 1 ? "s" : ""} removed`);
885
+ else if (nodeDelta < 0)
886
+ diffs.push(`${-nodeDelta} node${-nodeDelta !== 1 ? "s" : ""} restored`);
887
+ const addedVars = session.variables.filter(
888
+ (v) => !restored.variables.some((rv) => rv.name === v.name),
889
+ );
890
+ const removedVars = restored.variables.filter(
891
+ (v) => !session.variables.some((cv) => cv.name === v.name),
892
+ );
893
+ for (const v of addedVars) diffs.push(`removed $${v.name}`);
894
+ for (const v of removedVars) diffs.push(`restored $${v.name}`);
895
+ if (diffs.length === 0) {
896
+ const currArgsJson = JSON.stringify(
897
+ session.nodes
898
+ .filter((n) => n.kind === "field")
899
+ .map((n) => ({ id: n.id, args: (n as FieldProjectionNode).args })),
900
+ );
901
+ const restArgsJson = JSON.stringify(
902
+ restored.nodes
903
+ .filter((n) => n.kind === "field")
904
+ .map((n) => ({ id: n.id, args: (n as FieldProjectionNode).args })),
905
+ );
906
+ if (currArgsJson !== restArgsJson) diffs.push("argument values changed");
907
+ }
908
+
909
+ saveSession(restored);
910
+ const desc = diffs.length > 0 ? diffs.join(", ") : "last change";
911
+ console.log(`Undone: ${desc}`);
912
+ console.log("");
913
+ printQuery(restored);
914
+ break;
915
+ }
916
+
917
+ case "clone": {
918
+ const nameIdx = rest.indexOf("--name");
919
+ const cloneName =
920
+ nameIdx !== -1
921
+ ? rest[nameIdx + 1]
922
+ : rest[0] && !rest[0].startsWith("--")
923
+ ? rest[0]
924
+ : undefined;
925
+ const forceClone = rest.includes("--force") || rest.includes("-f");
926
+ const cloneResult = queryClone(sessionId, cloneName, { force: forceClone });
927
+
928
+ // Helper: collect args after a flag until the next --flag
929
+ const collectFlagArgs = (flag: string): string[] => {
930
+ const idx = rest.indexOf(flag);
931
+ if (idx === -1) return [];
932
+ const args: string[] = [];
933
+ for (let j = idx + 1; j < rest.length; j++) {
934
+ if (rest[j].startsWith("--") || rest[j].startsWith("-s")) break;
935
+ args.push(rest[j]);
936
+ }
937
+ return args;
938
+ };
939
+
940
+ // Apply flags in order: --unset first (remove inherited args), then --set, then --var.
941
+ // This ensures `clone --unset where --var $x path` works as expected:
942
+ // the old where is removed before the new variable binding creates a new one.
943
+ const clonedId = cloneResult.name ?? cloneResult.id;
944
+
945
+ // 1. Apply --unset to remove args from the cloned session
946
+ const unsetArgs = collectFlagArgs("--unset");
947
+ if (unsetArgs.length > 0 && clonedId) {
948
+ for (const path of unsetArgs) {
949
+ queryUnassign(clonedId, path);
950
+ }
951
+ }
952
+
953
+ // 2. Apply --set args to the cloned session (with full shorthand support)
954
+ const setArgs = collectFlagArgs("--set");
955
+ if (setArgs.length > 0 && clonedId) {
956
+ const parsed = parseInlineSetArgs(setArgs);
957
+ if (parsed.specs.length > 0) {
958
+ queryAssign(clonedId, parsed.specs);
959
+ }
960
+ for (const [varName, varPath] of parsed.deferredVarDefs) {
961
+ queryDefine(clonedId, [varName, varPath]);
962
+ }
963
+ if (parsed.hasCursor) {
964
+ const prefix = parsed.cursorFieldPrefix || "";
965
+ if (prefix) {
966
+ const cursorPath = `${prefix}/@args/after`;
967
+ queryDefine(clonedId, ["$after", cursorPath]);
968
+ // Auto-select pageInfo for pagination
969
+ const clonedSession = loadSession(clonedId);
970
+ try {
971
+ selectLeafInSession(clonedSession, `${prefix}/pageInfo/hasNextPage`);
972
+ selectLeafInSession(clonedSession, `${prefix}/pageInfo/endCursor`);
973
+ saveSession(clonedSession);
974
+ } catch {
975
+ /* ignore if already selected */
976
+ }
977
+ }
978
+ }
979
+ }
980
+
981
+ // 3. Apply --var to bind a variable on the cloned session
982
+ const varArgs = collectFlagArgs("--var");
983
+ if (varArgs.length >= 2 && clonedId) {
984
+ queryDefine(clonedId, varArgs);
985
+ }
986
+
987
+ // Apply --select to add fields to the cloned session
988
+ const selectArgs = collectFlagArgs("--select");
989
+ if (selectArgs.length > 0 && clonedId) {
990
+ const clonedSession = loadSession(clonedId);
991
+ for (const rawSpec of selectArgs) {
992
+ const colonIdx = rawSpec.lastIndexOf(":");
993
+ const afterColon = colonIdx > 0 ? rawSpec.slice(colonIdx + 1) : "";
994
+ let spec: string;
995
+ let alias: string | undefined;
996
+ if (colonIdx > 0 && afterColon.length > 0 && !/[./:]/.test(afterColon)) {
997
+ spec = rawSpec.slice(0, colonIdx);
998
+ alias = afterColon;
999
+ } else {
1000
+ spec = rawSpec;
1001
+ }
1002
+ try {
1003
+ selectLeafInSession(clonedSession, spec, alias);
1004
+ console.log(`Selected ${rawSpec}.`);
1005
+ } catch (error: any) {
1006
+ console.error(`Failed to select ${rawSpec}: ${error.message}`);
1007
+ }
1008
+ }
1009
+ saveSession(clonedSession);
1010
+ console.log("");
1011
+ printQuery(clonedSession);
1012
+ }
1013
+
1014
+ // Return cloned session info so the CLI can auto-activate it
1015
+ return { _clonedSessionId: clonedId } as any;
1016
+ }
1017
+
1018
+ case "optional": {
1019
+ if (rest.length === 0) {
1020
+ console.error(
1021
+ "Usage: optional <field-or-alias> [...] | optional --remove <field-or-alias> [...]",
1022
+ );
1023
+ return;
1024
+ }
1025
+ const removeMode = rest.includes("--remove");
1026
+ const targets = rest.filter((r) => !r.startsWith("--"));
1027
+ if (targets.length === 0) {
1028
+ console.error(
1029
+ "Usage: optional <field-or-alias> [...] | optional --remove <field-or-alias> [...]",
1030
+ );
1031
+ return;
1032
+ }
1033
+ const session = loadSession(sessionId);
1034
+ for (const target of targets) {
1035
+ const node = session.nodes.find(
1036
+ (n) =>
1037
+ n.kind === "field" &&
1038
+ ((n as FieldProjectionNode).alias === target ||
1039
+ (n as FieldProjectionNode).fieldName === target),
1040
+ );
1041
+ if (!node) {
1042
+ console.error(
1043
+ `No selected field matching "${target}". Use \`select ls\` to see selected fields.`,
1044
+ );
1045
+ continue;
1046
+ }
1047
+ if (removeMode) {
1048
+ node.directives = node.directives.filter((d) => d.name !== "optional");
1049
+ console.log(`Removed @optional from ${target}.`);
1050
+ } else {
1051
+ if (!node.directives.some((d) => d.name === "optional")) {
1052
+ node.directives.push({ name: "optional", args: {} });
1053
+ }
1054
+ console.log(`Added @optional to ${target}.`);
1055
+ }
1056
+ }
1057
+ saveSession(session);
1058
+ console.log("");
1059
+ printQuery(session);
1060
+ break;
1061
+ }
1062
+
1063
+ case "codegen": {
1064
+ await queryCodegen(sessionId, rest);
1065
+ break;
1066
+ }
1067
+
1068
+ default:
1069
+ throw new CommandError(`Unknown subcommand: ${subcommand}\n${formatHelp()}`);
1070
+ }
1071
+
1072
+ // Emit JSON result for all commands when in JSON mode
1073
+ if (jsonMode && subcommand !== "show" && subcommand !== "run" && subcommand !== "codegen") {
1074
+ console.log = origLog;
1075
+ console.error = origError;
1076
+ const result = buildJsonResult(subcommand, sessionId);
1077
+ origLog(JSON.stringify(result, null, 2));
1078
+ return result;
1079
+ }
1080
+ } finally {
1081
+ // Restore console in case of errors
1082
+ console.log = origLog;
1083
+ console.error = origError;
1084
+ }
1085
+ }