linmux 0.1.0

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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/bin/run.js +4 -0
  4. package/dist/commands/comment/create.js +94 -0
  5. package/dist/commands/comment/delete.js +74 -0
  6. package/dist/commands/comment/list.js +84 -0
  7. package/dist/commands/comment/update.js +80 -0
  8. package/dist/commands/cycle/current.js +78 -0
  9. package/dist/commands/cycle/list.js +84 -0
  10. package/dist/commands/cycle/move.js +91 -0
  11. package/dist/commands/describe.js +65 -0
  12. package/dist/commands/graphql/index.js +92 -0
  13. package/dist/commands/install-skill.js +54 -0
  14. package/dist/commands/issue/archive.js +75 -0
  15. package/dist/commands/issue/create.js +115 -0
  16. package/dist/commands/issue/get.js +84 -0
  17. package/dist/commands/issue/list.js +93 -0
  18. package/dist/commands/issue/purge.js +81 -0
  19. package/dist/commands/issue/search.js +109 -0
  20. package/dist/commands/issue/transition.js +91 -0
  21. package/dist/commands/issue/trash.js +75 -0
  22. package/dist/commands/issue/update.js +126 -0
  23. package/dist/commands/label/create.js +91 -0
  24. package/dist/commands/label/list.js +76 -0
  25. package/dist/commands/list-tools.js +47 -0
  26. package/dist/commands/me.js +71 -0
  27. package/dist/commands/project/create.js +101 -0
  28. package/dist/commands/project/get.js +83 -0
  29. package/dist/commands/project/list.js +75 -0
  30. package/dist/commands/project/update-status.js +99 -0
  31. package/dist/commands/project/update.js +99 -0
  32. package/dist/commands/raw/batch.js +85 -0
  33. package/dist/commands/raw/index.js +72 -0
  34. package/dist/commands/schema.js +69 -0
  35. package/dist/commands/state/list.js +77 -0
  36. package/dist/commands/team/get.js +73 -0
  37. package/dist/commands/team/list.js +73 -0
  38. package/dist/commands/whoami.js +71 -0
  39. package/dist/commands/workspace/add.js +97 -0
  40. package/dist/commands/workspace/list.js +47 -0
  41. package/dist/commands/workspace/remove.js +63 -0
  42. package/dist/commands/workspace/replace-token.js +89 -0
  43. package/dist/commands/workspace/use.js +54 -0
  44. package/dist/core/client/factory.js +28 -0
  45. package/dist/core/client/index.js +2 -0
  46. package/dist/core/config/index.js +4 -0
  47. package/dist/core/config/paths.js +30 -0
  48. package/dist/core/config/schema.js +36 -0
  49. package/dist/core/config/store.js +149 -0
  50. package/dist/core/errors/error.js +142 -0
  51. package/dist/core/errors/exit-codes.js +70 -0
  52. package/dist/core/output/envelope.js +53 -0
  53. package/dist/core/output/format.js +42 -0
  54. package/dist/core/output/index.js +3 -0
  55. package/dist/core/pagination/flags.js +29 -0
  56. package/dist/core/pagination/index.js +2 -0
  57. package/dist/core/projection/presets.js +116 -0
  58. package/dist/core/projection/project.js +282 -0
  59. package/dist/core/redact/redact.js +45 -0
  60. package/dist/core/resolvers/cycle.js +60 -0
  61. package/dist/core/resolvers/index.js +7 -0
  62. package/dist/core/resolvers/label.js +54 -0
  63. package/dist/core/resolvers/project-status.js +42 -0
  64. package/dist/core/resolvers/project.js +43 -0
  65. package/dist/core/resolvers/state.js +46 -0
  66. package/dist/core/resolvers/team.js +50 -0
  67. package/dist/core/transport/fetch-interceptor.js +109 -0
  68. package/dist/core/transport/index.js +3 -0
  69. package/dist/core/transport/rate-limit.js +167 -0
  70. package/dist/core/workspace/resolver.js +70 -0
  71. package/dist/core/workspace/write-guard.js +43 -0
  72. package/dist/generated/graphql.js +89428 -0
  73. package/dist/generated/operations.js +3013 -0
  74. package/dist/lib/comment-create-runtime.js +96 -0
  75. package/dist/lib/comment-delete-runtime.js +46 -0
  76. package/dist/lib/comment-list-runtime.js +182 -0
  77. package/dist/lib/comment-update-runtime.js +93 -0
  78. package/dist/lib/cycle-current-runtime.js +90 -0
  79. package/dist/lib/cycle-list-runtime.js +151 -0
  80. package/dist/lib/cycle-move-runtime.js +142 -0
  81. package/dist/lib/describe-runtime.js +180 -0
  82. package/dist/lib/filter-heuristics.js +59 -0
  83. package/dist/lib/graphql-runtime.js +202 -0
  84. package/dist/lib/include-fragments.js +73 -0
  85. package/dist/lib/install-skill-runtime.js +228 -0
  86. package/dist/lib/introspection-registry.js +488 -0
  87. package/dist/lib/issue-archive-runtime.js +89 -0
  88. package/dist/lib/issue-create-runtime.js +175 -0
  89. package/dist/lib/issue-get-runtime.js +153 -0
  90. package/dist/lib/issue-list-runtime.js +164 -0
  91. package/dist/lib/issue-purge-runtime.js +89 -0
  92. package/dist/lib/issue-search-runtime.js +114 -0
  93. package/dist/lib/issue-transition-runtime.js +131 -0
  94. package/dist/lib/issue-trash-runtime.js +84 -0
  95. package/dist/lib/issue-update-runtime.js +164 -0
  96. package/dist/lib/label-create-runtime.js +113 -0
  97. package/dist/lib/label-list-runtime.js +97 -0
  98. package/dist/lib/levenshtein.js +42 -0
  99. package/dist/lib/list-tools-runtime.js +38 -0
  100. package/dist/lib/me-runtime.js +55 -0
  101. package/dist/lib/project-create-runtime.js +103 -0
  102. package/dist/lib/project-get-runtime.js +134 -0
  103. package/dist/lib/project-list-runtime.js +84 -0
  104. package/dist/lib/project-update-runtime.js +110 -0
  105. package/dist/lib/project-update-status-runtime.js +91 -0
  106. package/dist/lib/raw-batch-runtime.js +229 -0
  107. package/dist/lib/raw-runtime.js +171 -0
  108. package/dist/lib/schema-loader.js +41 -0
  109. package/dist/lib/schema-runtime.js +65 -0
  110. package/dist/lib/state-list-runtime.js +93 -0
  111. package/dist/lib/team-get-runtime.js +55 -0
  112. package/dist/lib/team-list-runtime.js +52 -0
  113. package/dist/lib/workspace-runtime.js +112 -0
  114. package/dist/operations/_registry.zod.js +5337 -0
  115. package/oclif.manifest.json +3631 -0
  116. package/package.json +99 -0
  117. package/schema.graphql +30772 -0
  118. package/skills/linmux/SKILL.md +186 -0
@@ -0,0 +1,142 @@
1
+ import { LinearAgentError } from "../core/errors/error.js";
2
+ import { getLastComplexity, withFetchInterception } from "../core/transport/fetch-interceptor.js";
3
+ import { withRateLimitRetry } from "../core/transport/rate-limit.js";
4
+ import "../core/transport/index.js";
5
+ import { createLinearClient } from "../core/client/factory.js";
6
+ import "../core/client/index.js";
7
+ import { loadConfig } from "../core/config/store.js";
8
+ import "../core/config/index.js";
9
+ import { parseFields, project } from "../core/projection/project.js";
10
+ import { resolveWorkspace } from "../core/workspace/resolver.js";
11
+ import { requireExplicitWorkspaceForWrite } from "../core/workspace/write-guard.js";
12
+ import { ISSUE_IDENTIFIER_RE } from "./filter-heuristics.js";
13
+ import { resolveCycleId } from "../core/resolvers/cycle.js";
14
+ import "../core/resolvers/index.js";
15
+ //#region src/lib/cycle-move-runtime.ts
16
+ async function cycleMoveRuntime(input) {
17
+ const config = (input.loadConfigOverride ?? loadConfig)();
18
+ const envForResolver = {};
19
+ if (input.env.LINEAR_WORKSPACE !== void 0) envForResolver.LINEAR_WORKSPACE = input.env.LINEAR_WORKSPACE;
20
+ if (input.env.LINEAR_API_KEY !== void 0) envForResolver.LINEAR_API_KEY = input.env.LINEAR_API_KEY;
21
+ const resolved = resolveWorkspace({
22
+ flags: input.flags.workspace ? { workspace: input.flags.workspace } : {},
23
+ env: envForResolver,
24
+ config
25
+ });
26
+ requireExplicitWorkspaceForWrite(resolved, input.flags.allowActiveWorkspaceWrite ?? false);
27
+ const fields = parseFields(input.flags.fields ?? "defaults", "issue");
28
+ const client = (input.clientFactoryOverride ?? createLinearClient)(resolved);
29
+ return withFetchInterception(async () => {
30
+ const workspaceKey = resolved.name ?? "_api-key-env_";
31
+ const ref = input.args.issue;
32
+ const issueObj = await resolveIssue(client, ref, input.retryOptsOverride);
33
+ if (!issueObj) throw new LinearAgentError({
34
+ code: "ISSUE_NOT_FOUND",
35
+ message: `issue not found: ${ref}`,
36
+ details: { ref }
37
+ });
38
+ const issueIdRaw = issueObj.id;
39
+ if (typeof issueIdRaw !== "string") throw new LinearAgentError({
40
+ code: "ISSUE_NOT_FOUND",
41
+ message: `issue not found: ${ref}`,
42
+ details: { ref }
43
+ });
44
+ const issueId = issueIdRaw;
45
+ const teamRaw = await Promise.resolve(issueObj.team);
46
+ const teamId = teamRaw && typeof teamRaw === "object" && typeof teamRaw.id === "string" ? teamRaw.id : void 0;
47
+ if (!teamId) throw new LinearAgentError({
48
+ code: "ISSUE_NOT_FOUND",
49
+ message: `issue has no team: ${ref}`,
50
+ details: { ref }
51
+ });
52
+ const cycleId = await resolveCycleId(client, workspaceKey, teamId, input.args.to, input.retryOptsOverride);
53
+ const payload = await withRateLimitRetry(() => client.updateIssue(issueId, { cycleId }), input.retryOptsOverride);
54
+ if (!payload.success) throw LinearAgentError.linear.apiError({
55
+ message: "updateIssue returned success=false",
56
+ details: { lastSyncId: payload.lastSyncId }
57
+ });
58
+ let updated;
59
+ if (payload.issue !== void 0) {
60
+ const u = await Promise.resolve(payload.issue);
61
+ if (u !== void 0 && u !== null) updated = u;
62
+ }
63
+ let data;
64
+ if (updated) data = project(await hydrateForProjection(updated, fields), fields);
65
+ else data = {
66
+ id: issueId,
67
+ cycleId
68
+ };
69
+ const complexity = getLastComplexity();
70
+ const meta = {
71
+ workspace: resolved.name,
72
+ workspaceSource: resolved.source,
73
+ ...complexity !== void 0 ? { complexity } : {}
74
+ };
75
+ return {
76
+ data,
77
+ meta
78
+ };
79
+ });
80
+ }
81
+ /**
82
+ * Resolve an issue reference (ENG-123 or UUID) to its full Issue object.
83
+ * Mirrors `issue-update-runtime.ts`'s helper -- duplicated rather than
84
+ * extracted because the rule-of-three threshold hasn't been hit yet (issue-get,
85
+ * issue-transition, issue-update, and now cycle-move all carry their own
86
+ * copies; a future refactor may consolidate them in `src/lib/issue-ref.ts`).
87
+ */
88
+ async function resolveIssue(client, ref, retryOpts) {
89
+ const m = ISSUE_IDENTIFIER_RE.exec(ref);
90
+ if (m) {
91
+ const teamKey = m[1].toUpperCase();
92
+ const number = Number(m[2]);
93
+ const filter = {
94
+ team: { key: { eq: teamKey } },
95
+ number: { eq: number }
96
+ };
97
+ return (await withRateLimitRetry(() => client.issues({
98
+ filter,
99
+ first: 1
100
+ }), retryOpts)).nodes[0];
101
+ }
102
+ return await withRateLimitRetry(() => client.issue(ref), retryOpts) ?? void 0;
103
+ }
104
+ const RELATION_KEYS = new Set([
105
+ "state",
106
+ "assignee",
107
+ "team",
108
+ "project",
109
+ "cycle",
110
+ "parent"
111
+ ]);
112
+ async function hydrateForProjection(iss, spec) {
113
+ const needs = neededRelations(spec);
114
+ if (needs.size === 0) {
115
+ const out = {};
116
+ for (const k of Object.keys(iss)) if (!RELATION_KEYS.has(k)) out[k] = iss[k];
117
+ return out;
118
+ }
119
+ const hydrated = {};
120
+ for (const k of Object.keys(iss)) if (RELATION_KEYS.has(k)) {
121
+ if (needs.has(k)) {
122
+ const value = iss[k];
123
+ hydrated[k] = await resolveLazy(value);
124
+ }
125
+ } else hydrated[k] = iss[k];
126
+ return hydrated;
127
+ }
128
+ function neededRelations(spec) {
129
+ if (spec === "*") return new Set(RELATION_KEYS);
130
+ const out = /* @__PURE__ */ new Set();
131
+ for (const path of spec) {
132
+ const head = path.split(".")[0];
133
+ if (head && RELATION_KEYS.has(head)) out.add(head);
134
+ }
135
+ return out;
136
+ }
137
+ async function resolveLazy(value) {
138
+ if (value && typeof value.then === "function") return await value;
139
+ return value;
140
+ }
141
+ //#endregion
142
+ export { cycleMoveRuntime };
@@ -0,0 +1,180 @@
1
+ import { LinearAgentError } from "../core/errors/error.js";
2
+ import { OPERATION_REGISTRY } from "../generated/operations.js";
3
+ import { CURATED_REGISTRY } from "./introspection-registry.js";
4
+ import { suggestClosest } from "./levenshtein.js";
5
+ import { getLinearSchema } from "./schema-loader.js";
6
+ import { getNamedType, isInterfaceType, isObjectType, isUnionType, printType } from "graphql";
7
+ import { z } from "zod";
8
+ //#region src/lib/describe-runtime.ts
9
+ /**
10
+ * `describeRuntime` — Phase 4 PLAN 04-03, INT-02.
11
+ *
12
+ * Returns input JSON Schema (from Zod) + examples for curated commands,
13
+ * or input JSON Schema + SDL fragment for raw operations. Zero network calls.
14
+ *
15
+ * Disambiguation algorithm (CONTEXT.md § Specifics, RESEARCH § Pitfall 6):
16
+ * - target.includes(' ') || /^[a-z]/.test(target) → curated path
17
+ * - PascalCase single token (uppercase start, no spaces) → raw path
18
+ * - Not found → DESCRIBE_COMMAND_NOT_FOUND with Levenshtein suggestions
19
+ *
20
+ * Exports:
21
+ * - `describeRuntime` — named runtime function (test seam)
22
+ * - `describeRuntime` is the only export; command shim lives in src/commands/describe.ts
23
+ */
24
+ /**
25
+ * The Phase 1 envelope output schema for curated commands.
26
+ *
27
+ * Mirrors the real envelope contract from `src/core/output/envelope.ts`:
28
+ * - SuccessEnvelope: { $apiVersion, ok: true, data, meta: Meta }
29
+ * - SuccessEnvelopeNoMeta: { $apiVersion, ok: true, data } -- emitted when
30
+ * `--quiet` or `--no-meta` is set (Phase 6 PLAN 06-01, MNT-02).
31
+ * - FailureEnvelope: { $apiVersion, ok: false, error, meta: FailureMeta }
32
+ *
33
+ * Per CONTEXT.md § describe shape — v1 leaves `data: z.unknown()`.
34
+ * Per-entity output schemas deferred to Phase 5/6.
35
+ *
36
+ * Phase 6 PLAN 06-01 (MNT-02) extends this to a 3-way union to reflect the
37
+ * `--no-meta` / `--quiet` envelope variants.
38
+ *
39
+ * If you change the envelope shape in `src/core/output/envelope.ts`, the
40
+ * snapshot tests in `test/lib/describe-runtime.test.ts` and
41
+ * `test/commands/describe.test.ts` will fail until this schema is brought
42
+ * back into parity — that's intentional.
43
+ */
44
+ const PageInfoSchema = z.object({
45
+ hasNextPage: z.boolean(),
46
+ endCursor: z.string().nullable(),
47
+ hasPreviousPage: z.boolean(),
48
+ startCursor: z.string().nullable()
49
+ });
50
+ const ComplexitySchema = z.object({
51
+ cost: z.number(),
52
+ remaining: z.number()
53
+ });
54
+ const BatchSchema = z.object({
55
+ count: z.number(),
56
+ kinds: z.object({
57
+ query: z.number(),
58
+ mutation: z.number()
59
+ })
60
+ });
61
+ const WorkspaceSourceSchema = z.enum([
62
+ "flag",
63
+ "env",
64
+ "active",
65
+ "single",
66
+ "api-key-env"
67
+ ]);
68
+ const MetaSchema = z.object({
69
+ command: z.string(),
70
+ workspace: z.string().nullable().optional(),
71
+ workspaceSource: WorkspaceSourceSchema.optional(),
72
+ pageInfo: PageInfoSchema.optional(),
73
+ complexity: ComplexitySchema.optional(),
74
+ totalCount: z.number().optional(),
75
+ batch: BatchSchema.optional()
76
+ });
77
+ const FailureMetaSchema = z.object({
78
+ command: z.string(),
79
+ workspace: z.string().nullable().optional(),
80
+ workspaceSource: WorkspaceSourceSchema.optional()
81
+ });
82
+ const SuccessEnvelopeSchema = z.object({
83
+ $apiVersion: z.literal("1"),
84
+ ok: z.literal(true),
85
+ data: z.unknown(),
86
+ meta: MetaSchema
87
+ });
88
+ const SuccessEnvelopeNoMetaSchema = z.object({
89
+ $apiVersion: z.literal("1"),
90
+ ok: z.literal(true),
91
+ data: z.unknown()
92
+ });
93
+ const FailureEnvelopeSchema = z.object({
94
+ $apiVersion: z.literal("1"),
95
+ ok: z.literal(false),
96
+ error: z.object({
97
+ code: z.string(),
98
+ message: z.string(),
99
+ transient: z.boolean(),
100
+ retryAfterMs: z.number().optional(),
101
+ details: z.record(z.string(), z.unknown()).optional()
102
+ }),
103
+ meta: FailureMetaSchema
104
+ });
105
+ const EnvelopeOutputSchema = z.union([
106
+ SuccessEnvelopeSchema,
107
+ SuccessEnvelopeNoMetaSchema,
108
+ FailureEnvelopeSchema
109
+ ]);
110
+ /**
111
+ * Standard z.toJSONSchema() options per CONTEXT.md § stack contract.
112
+ * Always use draft-2020-12 + unrepresentable:'any' to avoid crashes on
113
+ * z.transform() or z.unknown() fields in raw varsSchemas (RESEARCH Pitfall 1, 4).
114
+ */
115
+ const JSON_SCHEMA_OPTS = {
116
+ target: "draft-2020-12",
117
+ unrepresentable: "any"
118
+ };
119
+ /**
120
+ * Convert PascalCase operation name to camelCase for root-type field lookup.
121
+ * e.g. 'IssueCreate' → 'issueCreate'
122
+ */
123
+ function toCamelCase(pascal) {
124
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
125
+ }
126
+ /**
127
+ * Get a compact SDL fragment for the return type of a raw operation.
128
+ * Falls back to a comment string if resolution fails (acceptable for v1).
129
+ */
130
+ function getSdlFragment(operationName, kind) {
131
+ try {
132
+ const schema = getLinearSchema();
133
+ const rootType = kind === "query" ? schema.getQueryType() : schema.getMutationType();
134
+ const fieldName = toCamelCase(operationName);
135
+ const field = rootType?.getFields()[fieldName];
136
+ const returnType = field ? getNamedType(field.type) : void 0;
137
+ if (returnType && (isObjectType(returnType) || isInterfaceType(returnType) || isUnionType(returnType))) return printType(returnType);
138
+ return `# Return type: ${returnType?.name ?? "unknown"}\n# Use the 'schema' command for full type definitions.`;
139
+ } catch {
140
+ return `# Return type resolution unavailable. Use the 'schema' command for full definitions.`;
141
+ }
142
+ }
143
+ /**
144
+ * Core runtime for `linmux describe <command>`.
145
+ *
146
+ * @throws {LinearAgentError} with code DESCRIBE_COMMAND_NOT_FOUND when target is not found
147
+ */
148
+ async function describeRuntime(args) {
149
+ const target = args.args.command;
150
+ if (target.includes(" ") || /^[a-z]/.test(target)) {
151
+ const entry = CURATED_REGISTRY.find((e) => e.id === target);
152
+ if (entry) return { data: {
153
+ command: target,
154
+ kind: "curated",
155
+ input: z.toJSONSchema(entry.inputSchema, JSON_SCHEMA_OPTS),
156
+ output: z.toJSONSchema(EnvelopeOutputSchema, JSON_SCHEMA_OPTS),
157
+ examples: entry.examples
158
+ } };
159
+ } else {
160
+ const entry = Object.hasOwn(OPERATION_REGISTRY, target) ? OPERATION_REGISTRY[target] : void 0;
161
+ if (entry) return { data: {
162
+ command: target,
163
+ kind: "raw",
164
+ input: z.toJSONSchema(entry.varsSchema, JSON_SCHEMA_OPTS),
165
+ output: getSdlFragment(target, entry.kind),
166
+ examples: []
167
+ } };
168
+ }
169
+ const suggestions = suggestClosest(target, [...CURATED_REGISTRY.map((e) => e.id), ...Object.keys(OPERATION_REGISTRY)], 3);
170
+ throw new LinearAgentError({
171
+ code: "DESCRIBE_COMMAND_NOT_FOUND",
172
+ message: `unknown command: '${target}'. Did you mean: ${suggestions.join(", ")}?`,
173
+ details: {
174
+ target,
175
+ suggestions
176
+ }
177
+ });
178
+ }
179
+ //#endregion
180
+ export { describeRuntime };
@@ -0,0 +1,59 @@
1
+ //#region src/lib/filter-heuristics.ts
2
+ /**
3
+ * Filter heuristics — UUID/email/team-key/issue-identifier routing for
4
+ * entity filter flags. Extracted from `issue-list-runtime.ts` (Phase 1) so
5
+ * `issue search`, future entity lists, and resolvers share the same shape
6
+ * detection without drift (Phase 2 PLAN 02-01 Task 2; CONTEXT § Specifics).
7
+ *
8
+ * `buildIssueFilter` mirrors the original Phase 1 behavior verbatim for the
9
+ * three flags it accepted (`state`, `assignee`, `team`) and adds three
10
+ * forward-looking flags (`label`, `project`, `cycle`) that downstream Phase
11
+ * 2 plans will surface in their command flags.
12
+ */
13
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
15
+ const TEAM_KEY_RE = /^[A-Z0-9]{2,6}$/i;
16
+ /** Linear issue identifiers like `ENG-123`. */
17
+ const ISSUE_IDENTIFIER_RE = /^([A-Z][A-Z0-9]+)-(\d+)$/i;
18
+ /**
19
+ * Classify a free-form identifier string into the routing bucket the typed
20
+ * SDK filter shape expects (UUID → `id`, email → `email`, ENG → `key`,
21
+ * `me` → `isMe`, `ENG-123` → issue identifier, anything else → name).
22
+ *
23
+ * The `me` literal is checked first because it would otherwise match the
24
+ * 2-char team-key pattern.
25
+ */
26
+ function classifyIdentifier(value) {
27
+ if (value === "me") return "me";
28
+ if (UUID_RE.test(value)) return "uuid";
29
+ if (EMAIL_RE.test(value)) return "email";
30
+ if (ISSUE_IDENTIFIER_RE.test(value)) return "issueIdentifier";
31
+ if (TEAM_KEY_RE.test(value)) return "teamKey";
32
+ return "name";
33
+ }
34
+ /**
35
+ * Build a typed-SDK `IssueFilter` from free-form CLI flag values.
36
+ *
37
+ * Behavior matches Phase 1 `issue-list-runtime.ts:140-174` for the three
38
+ * original flags (`state`, `assignee`, `team`) — verified by Test 31 in the
39
+ * Plan 02-01 Task 2 test suite, which is a regression check against the
40
+ * original implementation.
41
+ */
42
+ function buildIssueFilter(flags) {
43
+ const filter = {};
44
+ if (flags.state) filter.state = UUID_RE.test(flags.state) ? { id: { eq: flags.state } } : { name: { eq: flags.state } };
45
+ if (flags.assignee) if (flags.assignee === "me") filter.assignee = { isMe: { eq: true } };
46
+ else if (EMAIL_RE.test(flags.assignee)) filter.assignee = { email: { eq: flags.assignee } };
47
+ else if (UUID_RE.test(flags.assignee)) filter.assignee = { id: { eq: flags.assignee } };
48
+ else filter.assignee = { email: { eq: flags.assignee } };
49
+ if (flags.team) if (UUID_RE.test(flags.team)) filter.team = { id: { eq: flags.team } };
50
+ else if (TEAM_KEY_RE.test(flags.team)) filter.team = { key: { eq: flags.team.toUpperCase() } };
51
+ else filter.team = { name: { eq: flags.team } };
52
+ if (flags.label) filter.labels = UUID_RE.test(flags.label) ? { id: { eq: flags.label } } : { name: { eq: flags.label } };
53
+ if (flags.project) filter.project = UUID_RE.test(flags.project) ? { id: { eq: flags.project } } : { name: { eq: flags.project } };
54
+ if (flags.cycle) filter.cycle = { id: { eq: flags.cycle } };
55
+ if (Object.keys(filter).length === 0) return void 0;
56
+ return filter;
57
+ }
58
+ //#endregion
59
+ export { EMAIL_RE, ISSUE_IDENTIFIER_RE, TEAM_KEY_RE, UUID_RE, buildIssueFilter, classifyIdentifier };
@@ -0,0 +1,202 @@
1
+ import { LinearAgentError } from "../core/errors/error.js";
2
+ import { getLinearSchema } from "./schema-loader.js";
3
+ import { redact } from "../core/redact/redact.js";
4
+ import { getLastComplexity, withFetchInterception } from "../core/transport/fetch-interceptor.js";
5
+ import { withRateLimitRetry } from "../core/transport/rate-limit.js";
6
+ import "../core/transport/index.js";
7
+ import { createLinearClient } from "../core/client/factory.js";
8
+ import "../core/client/index.js";
9
+ import { loadConfig } from "../core/config/store.js";
10
+ import "../core/config/index.js";
11
+ import { resolveWorkspace } from "../core/workspace/resolver.js";
12
+ import { requireExplicitWorkspaceForWrite } from "../core/workspace/write-guard.js";
13
+ import { parse, validate } from "graphql";
14
+ import { readFile } from "node:fs/promises";
15
+ //#region src/lib/graphql-runtime.ts
16
+ /**
17
+ * `graphql-runtime` — 8-step pipeline for the free-form GraphQL command.
18
+ *
19
+ * Phase 3 PLAN 03-03, RAW-03.
20
+ *
21
+ * Pipeline:
22
+ * 1. resolveWorkspace (same precedence chain as all commands)
23
+ * 2. Load query text: `--query='...'` (inline) OR `--query=@file.graphql` (file ref)
24
+ * ENOENT/EACCES → GRAPHQL_QUERY_FILE_NOT_FOUND
25
+ * 3. parse(queryText) — try/catch; syntax errors → GRAPHQL_VALIDATION_FAILED (kind=parse)
26
+ * 4. validate(getLinearSchema(), document) — non-empty → GRAPHQL_VALIDATION_FAILED (kind=validate)
27
+ * Runs against vendored schema BEFORE any Linear API call (saves quota, gives precise errors)
28
+ * 5. Detect operation kind via .find(d => d.kind === 'OperationDefinition')
29
+ * Pitfall 5: NOT definitions[0] — first def could be a FragmentDefinition
30
+ * 'subscription' → OPERATION_SUBSCRIPTIONS_UNSUPPORTED (exit 2)
31
+ * 6. If 'mutation': WSP-06 FIRST (requireExplicitWorkspaceForWrite), THEN --allow-mutations check
32
+ * Order matters — missing workspace is the more dangerous mistake
33
+ * 7. Load vars: inline JSON OR @file.json (file ref takes precedence)
34
+ * 8. Dispatch via client.client.rawRequest wrapped in withFetchInterception + withRateLimitRetry
35
+ *
36
+ * Error codes introduced by this plan:
37
+ * - GRAPHQL_QUERY_FILE_NOT_FOUND (exit 2) — @file.graphql missing
38
+ * - GRAPHQL_VALIDATION_FAILED (exit 12) — parse or validate failure
39
+ * - OPERATION_SUBSCRIPTIONS_UNSUPPORTED (exit 2) — subscription op rejected
40
+ *
41
+ * Codes reused from prior plans (no new snapshots needed):
42
+ * - RAW_MUTATION_REQUIRES_FLAG (exit 2) — from 03-02
43
+ * - WORKSPACE_REQUIRED_FOR_WRITE (exit 10) — from Phase 1 WSP-06
44
+ * - LINEAR_API_ERROR (exit 13) — from Phase 1
45
+ */
46
+ async function runGraphql(input) {
47
+ const config = (input.loadConfigOverride ?? loadConfig)();
48
+ const envForResolver = {};
49
+ if (input.env.LINEAR_WORKSPACE !== void 0) envForResolver.LINEAR_WORKSPACE = input.env.LINEAR_WORKSPACE;
50
+ if (input.env.LINEAR_API_KEY !== void 0) envForResolver.LINEAR_API_KEY = input.env.LINEAR_API_KEY;
51
+ const resolved = resolveWorkspace({
52
+ flags: input.flags.workspace ? { workspace: input.flags.workspace } : {},
53
+ env: envForResolver,
54
+ config
55
+ });
56
+ const queryText = await loadQueryText(input.flags.query);
57
+ let document;
58
+ try {
59
+ document = parse(queryText);
60
+ } catch (err) {
61
+ throw new LinearAgentError({
62
+ code: "GRAPHQL_VALIDATION_FAILED",
63
+ message: `query parse failed: ${err.message}`,
64
+ details: {
65
+ kind: "parse",
66
+ cause: err.message
67
+ }
68
+ });
69
+ }
70
+ const validationErrors = validate(getLinearSchema(), document);
71
+ if (validationErrors.length > 0) throw new LinearAgentError({
72
+ code: "GRAPHQL_VALIDATION_FAILED",
73
+ message: `query failed schema validation (${validationErrors.length} error${validationErrors.length === 1 ? "" : "s"})`,
74
+ details: {
75
+ kind: "validate",
76
+ errors: validationErrors.map((e) => ({
77
+ message: e.message,
78
+ locations: e.locations,
79
+ path: e.path
80
+ }))
81
+ }
82
+ });
83
+ const opDef = document.definitions.find((d) => d.kind === "OperationDefinition");
84
+ if (!opDef || opDef.kind !== "OperationDefinition") throw new LinearAgentError({
85
+ code: "GRAPHQL_VALIDATION_FAILED",
86
+ message: "query has no operation definition",
87
+ details: { kind: "no-operation" }
88
+ });
89
+ if (opDef.operation === "subscription") throw new LinearAgentError({
90
+ code: "OPERATION_SUBSCRIPTIONS_UNSUPPORTED",
91
+ message: "subscriptions are not supported in v1 — use query or mutation operations"
92
+ });
93
+ if (opDef.operation === "mutation") {
94
+ requireExplicitWorkspaceForWrite(resolved, input.flags["allow-active-workspace-write"] ?? false);
95
+ if (!input.flags["allow-mutations"]) throw new LinearAgentError({
96
+ code: "RAW_MUTATION_REQUIRES_FLAG",
97
+ message: "mutation queries require --allow-mutations to prevent accidental data modification"
98
+ });
99
+ }
100
+ const vars = await loadVars(input.flags.vars);
101
+ if (input.mockRawRequest) {
102
+ const response = await input.mockRawRequest(queryText, vars);
103
+ if (response.error !== void 0 || response.data === void 0) throw new LinearAgentError({
104
+ code: "LINEAR_API_ERROR",
105
+ message: redact(response.error ?? "graphql request returned no data"),
106
+ details: {
107
+ kind: "graphql-runtime",
108
+ cause: response.error !== void 0 ? redact(response.error) : void 0
109
+ }
110
+ });
111
+ const complexity = getLastComplexity();
112
+ return {
113
+ data: response.data,
114
+ meta: {
115
+ workspace: resolved.name,
116
+ workspaceSource: resolved.source,
117
+ ...complexity !== void 0 ? { complexity } : {}
118
+ }
119
+ };
120
+ }
121
+ const client = createLinearClient(resolved);
122
+ return withFetchInterception(async () => {
123
+ const response = await withRateLimitRetry(() => client.client.rawRequest(queryText, vars), input.retryOptsOverride);
124
+ if (response.error !== void 0 || response.data === void 0) throw new LinearAgentError({
125
+ code: "LINEAR_API_ERROR",
126
+ message: redact(response.error ?? "graphql request returned no data"),
127
+ details: {
128
+ kind: "graphql-runtime",
129
+ cause: response.error !== void 0 ? redact(response.error) : void 0
130
+ }
131
+ });
132
+ const complexity = getLastComplexity();
133
+ return {
134
+ data: response.data,
135
+ meta: {
136
+ workspace: resolved.name,
137
+ workspaceSource: resolved.source,
138
+ ...complexity !== void 0 ? { complexity } : {}
139
+ }
140
+ };
141
+ });
142
+ }
143
+ /**
144
+ * Load query text from inline string or @file.graphql reference.
145
+ * If the query starts with '@', treat the rest as a filesystem path.
146
+ */
147
+ async function loadQueryText(query) {
148
+ if (!query.startsWith("@")) return query;
149
+ const filePath = query.slice(1);
150
+ try {
151
+ return await readFile(filePath, "utf8");
152
+ } catch (err) {
153
+ const code = err.code;
154
+ throw new LinearAgentError({
155
+ code: "GRAPHQL_QUERY_FILE_NOT_FOUND",
156
+ message: `query file not found or not readable: ${filePath}`,
157
+ details: {
158
+ path: filePath,
159
+ cause: code ?? err.message
160
+ }
161
+ });
162
+ }
163
+ }
164
+ /**
165
+ * Load vars from inline JSON string or @file.json reference.
166
+ * File reference takes precedence if both are provided (file path starts with '@').
167
+ * Returns an empty object if no vars provided.
168
+ *
169
+ * Note: This helper is intentionally duplicated from raw-runtime (not shared)
170
+ * to maintain cross-plan independence for wave-2 parallel execution (03-02 and
171
+ * 03-03 run concurrently; sharing a helper file would create ordering constraints).
172
+ */
173
+ async function loadVars(vars) {
174
+ if (!vars) return {};
175
+ if (vars.startsWith("@")) {
176
+ const filePath = vars.slice(1);
177
+ try {
178
+ const text = await readFile(filePath, "utf8");
179
+ return JSON.parse(text);
180
+ } catch (err) {
181
+ throw new LinearAgentError({
182
+ code: "RAW_VARS_INVALID",
183
+ message: `failed to read or parse vars file: ${filePath}`,
184
+ details: {
185
+ path: filePath,
186
+ cause: err.message
187
+ }
188
+ });
189
+ }
190
+ }
191
+ try {
192
+ return JSON.parse(vars);
193
+ } catch (err) {
194
+ throw new LinearAgentError({
195
+ code: "RAW_VARS_INVALID",
196
+ message: "failed to parse --vars as JSON",
197
+ details: { cause: err.message }
198
+ });
199
+ }
200
+ }
201
+ //#endregion
202
+ export { runGraphql };
@@ -0,0 +1,73 @@
1
+ import { LinearAgentError } from "../core/errors/error.js";
2
+ //#region src/lib/include-fragments.ts
3
+ /**
4
+ * `--include` fragment map for Phase 3 RAW-04.
5
+ *
6
+ * Maps each Tier-1 command name to a record of include-key → inline GraphQL
7
+ * fragment text. The fragment text is inlined directly into the composed
8
+ * query string inside the `nodes { ... }` selection set.
9
+ *
10
+ * Security: include keys are validated by exact-match lookup against this
11
+ * map (T-03-04-INCLUDE-INJECTION). Unknown keys → INVALID_INCLUDE BEFORE
12
+ * any string concat. Fragment text is hand-authored, NOT user-supplied.
13
+ *
14
+ * Single-round-trip guarantee (T-03-04-N-PLUS-1): agents requesting
15
+ * `issue list --include comments` get one composed rawRequest call instead
16
+ * of 1+N separate SDK calls.
17
+ */
18
+ const INCLUDE_FRAGMENT_MAP = {
19
+ "issue list": {
20
+ comments: "comments(first: 50) { nodes { id body createdAt user { id name } } }",
21
+ labels: "labels(first: 50) { nodes { id name color } }",
22
+ attachments: "attachments(first: 25) { nodes { id title url } }",
23
+ subscribers: "subscribers(first: 50) { nodes { id name } }",
24
+ history: "history(first: 50) { nodes { id createdAt actor { id name } } }"
25
+ },
26
+ "issue get": {
27
+ comments: "comments(first: 50) { nodes { id body createdAt user { id name } } }",
28
+ labels: "labels(first: 50) { nodes { id name color } }",
29
+ attachments: "attachments(first: 25) { nodes { id title url } }",
30
+ subscribers: "subscribers(first: 50) { nodes { id name } }",
31
+ history: "history(first: 50) { nodes { id createdAt actor { id name } } }"
32
+ },
33
+ "comment list": {
34
+ reactions: "reactions { id emoji }",
35
+ parent: "parent { id body }"
36
+ },
37
+ "project get": {
38
+ members: "members(first: 50) { nodes { id name email } }",
39
+ teams: "teams(first: 25) { nodes { id key name } }",
40
+ projectMilestones: "projectMilestones(first: 50) { nodes { id name targetDate } }",
41
+ documents: "documents(first: 25) { nodes { id title } }"
42
+ },
43
+ "cycle list": { issues: "issues(first: 50) { nodes { id identifier title } }" }
44
+ };
45
+ /**
46
+ * Validates every requested include key against the command's allowed map.
47
+ * Returns concatenated fragment text (each fragment joined with newline +
48
+ * 6-space indent to fit inside the typical `nodes { ... }` indentation).
49
+ *
50
+ * Returns '' when requested is empty (runtimes branch on empty BEFORE calling).
51
+ *
52
+ * Throws LinearAgentError with code 'INVALID_INCLUDE' (exit 2) and
53
+ * details.allowed (sorted alphabetically) + details.unknown on any miss.
54
+ * The error is thrown BEFORE any string concat — unknown keys never reach
55
+ * the composed query string (T-03-04-INCLUDE-INJECTION mitigation).
56
+ */
57
+ function validateAndMergeIncludes(commandName, requested) {
58
+ if (requested.length === 0) return "";
59
+ const allowed = INCLUDE_FRAGMENT_MAP[commandName];
60
+ const allowedKeys = Object.keys(allowed).sort();
61
+ const unknown = requested.filter((k) => !(k in allowed));
62
+ if (unknown.length > 0) throw new LinearAgentError({
63
+ code: "INVALID_INCLUDE",
64
+ message: `unknown --include keys for ${commandName}: ${unknown.join(", ")}`,
65
+ details: {
66
+ unknown,
67
+ allowed: allowedKeys
68
+ }
69
+ });
70
+ return requested.map((k) => allowed[k]).join("\n ");
71
+ }
72
+ //#endregion
73
+ export { INCLUDE_FRAGMENT_MAP, validateAndMergeIncludes };