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,53 @@
1
+ //#region src/core/output/envelope.ts
2
+ /**
3
+ * Build a stable Meta record with the canonical key order:
4
+ * command, workspace, workspaceSource, pageInfo, complexity, totalCount, batch
5
+ * Optional fields with `undefined` values are omitted; `null` workspaces
6
+ * are preserved (they are meaningful — "ran via LINEAR_API_KEY env").
7
+ *
8
+ * `complexity` and `totalCount` are Phase 2 additions (PLAN 02-01). `batch`
9
+ * is a Phase 3 PLAN 03-01 addition. They are appended in the order they
10
+ * were added so any envelope that omits them serializes identically to its
11
+ * Phase 1/2 byte-for-byte form.
12
+ */
13
+ function buildMeta(meta) {
14
+ const out = { command: meta.command };
15
+ if (meta.workspace !== void 0) out.workspace = meta.workspace;
16
+ if (meta.workspaceSource !== void 0) out.workspaceSource = meta.workspaceSource;
17
+ if (meta.pageInfo !== void 0) out.pageInfo = meta.pageInfo;
18
+ if (meta.complexity !== void 0) out.complexity = meta.complexity;
19
+ if (meta.totalCount !== void 0) out.totalCount = meta.totalCount;
20
+ if (meta.batch !== void 0) out.batch = meta.batch;
21
+ return out;
22
+ }
23
+ function buildFailureMeta(meta) {
24
+ const out = { command: meta.command };
25
+ if (meta.workspace !== void 0) out.workspace = meta.workspace;
26
+ if (meta.workspaceSource !== void 0) out.workspaceSource = meta.workspaceSource;
27
+ return out;
28
+ }
29
+ function success(data, meta) {
30
+ return {
31
+ $apiVersion: "1",
32
+ ok: true,
33
+ data,
34
+ meta: buildMeta(meta)
35
+ };
36
+ }
37
+ function failure(error, meta) {
38
+ const errOut = {
39
+ code: error.code,
40
+ message: error.message,
41
+ transient: error.transient
42
+ };
43
+ if (error.retryAfterMs !== void 0) errOut.retryAfterMs = error.retryAfterMs;
44
+ if (error.details !== void 0) errOut.details = error.details;
45
+ return {
46
+ $apiVersion: "1",
47
+ ok: false,
48
+ error: errOut,
49
+ meta: buildFailureMeta(meta)
50
+ };
51
+ }
52
+ //#endregion
53
+ export { failure, success };
@@ -0,0 +1,42 @@
1
+ import { redact } from "../redact/redact.js";
2
+ import pc from "picocolors";
3
+ //#region src/core/output/format.ts
4
+ function format(envelope, opts) {
5
+ const redacted = redact(envelope);
6
+ if (!opts.pretty) return { stdout: `${JSON.stringify(redacted)}\n` };
7
+ if (redacted.ok) {
8
+ if (!("meta" in redacted)) return { stdout: `${JSON.stringify(redacted)}\n` };
9
+ return { stdout: renderSuccessPretty(redacted) };
10
+ }
11
+ return {
12
+ stdout: renderFailurePretty(redacted),
13
+ stderr: renderFailureStderrSummary(redacted)
14
+ };
15
+ }
16
+ function renderSuccessPretty(env) {
17
+ const lines = [];
18
+ const header = `${env.meta.command}${env.meta.workspace ? ` · ${env.meta.workspace}` : ""}`;
19
+ lines.push(pc.dim(`# ${header}`));
20
+ lines.push(JSON.stringify(env.data, null, 2));
21
+ if (env.meta.pageInfo) {
22
+ const pi = env.meta.pageInfo;
23
+ lines.push(pc.dim(`# pageInfo: hasNextPage=${pi.hasNextPage}${pi.endCursor ? ` endCursor=${pi.endCursor}` : ""}`));
24
+ }
25
+ return `${lines.join("\n")}\n`;
26
+ }
27
+ function renderFailurePretty(env) {
28
+ const lines = [];
29
+ lines.push(`${pc.bold(pc.red("error:"))} ${env.error.code} — ${env.error.message}`);
30
+ if (env.error.transient) {
31
+ const retry = env.error.retryAfterMs !== void 0 ? ` (retry after ${env.error.retryAfterMs}ms)` : "";
32
+ lines.push(pc.dim(`# transient${retry} — safe to retry`));
33
+ }
34
+ if (env.error.details) for (const [k, v] of Object.entries(env.error.details)) lines.push(` ${pc.dim(k)}: ${typeof v === "string" ? v : JSON.stringify(v)}`);
35
+ lines.push(pc.dim("# see stderr for one-liner; full envelope on stdout above"));
36
+ return `${lines.join("\n")}\n`;
37
+ }
38
+ function renderFailureStderrSummary(env) {
39
+ return `${pc.bold(pc.red("error:"))} ${env.error.code} — ${env.error.message}\n`;
40
+ }
41
+ //#endregion
42
+ export { format };
@@ -0,0 +1,3 @@
1
+ import "./envelope.js";
2
+ import "./format.js";
3
+ export {};
@@ -0,0 +1,29 @@
1
+ import { LinearAgentError } from "../errors/error.js";
2
+ import { Flags } from "@oclif/core";
3
+ const PAGINATION_FLAGS = {
4
+ limit: Flags.integer({
5
+ description: `Page size (default 25, max 100)`,
6
+ default: 25,
7
+ min: 1,
8
+ max: 100
9
+ }),
10
+ cursor: Flags.string({ description: "Opaque cursor from a previous response's meta.pageInfo.endCursor" })
11
+ };
12
+ function parsePagination(input) {
13
+ const limit = input.limit ?? 25;
14
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) throw new LinearAgentError({
15
+ code: "USAGE_ERROR",
16
+ message: `--limit must be an integer between 1 and 100`,
17
+ details: {
18
+ received: limit,
19
+ min: 1,
20
+ max: 100
21
+ }
22
+ });
23
+ return {
24
+ first: limit,
25
+ after: input.cursor
26
+ };
27
+ }
28
+ //#endregion
29
+ export { PAGINATION_FLAGS, parsePagination };
@@ -0,0 +1,2 @@
1
+ import "./flags.js";
2
+ export {};
@@ -0,0 +1,116 @@
1
+ const FIELD_PRESETS = {
2
+ issue: {
3
+ ids: ["id", "identifier"],
4
+ defaults: [
5
+ "id",
6
+ "identifier",
7
+ "title",
8
+ "state.name",
9
+ "priority",
10
+ "assignee.email",
11
+ "team.key",
12
+ "updatedAt"
13
+ ],
14
+ full: "*"
15
+ },
16
+ comment: {
17
+ ids: ["id"],
18
+ defaults: [
19
+ "id",
20
+ "body",
21
+ "user.email",
22
+ "user.name",
23
+ "issue.identifier",
24
+ "parent.id",
25
+ "createdAt",
26
+ "updatedAt"
27
+ ],
28
+ full: "*"
29
+ },
30
+ project: {
31
+ ids: ["id"],
32
+ defaults: [
33
+ "id",
34
+ "name",
35
+ "state",
36
+ "progress",
37
+ "targetDate",
38
+ "lead.email",
39
+ "description",
40
+ "updatedAt"
41
+ ],
42
+ full: "*"
43
+ },
44
+ cycle: {
45
+ ids: ["id"],
46
+ defaults: [
47
+ "id",
48
+ "number",
49
+ "name",
50
+ "startsAt",
51
+ "endsAt",
52
+ "progress",
53
+ "team.key",
54
+ "isActive"
55
+ ],
56
+ full: "*"
57
+ },
58
+ team: {
59
+ ids: ["id", "key"],
60
+ defaults: [
61
+ "id",
62
+ "key",
63
+ "name",
64
+ "description",
65
+ "color",
66
+ "private",
67
+ "createdAt",
68
+ "cycleEnabled"
69
+ ],
70
+ full: "*"
71
+ },
72
+ label: {
73
+ ids: ["id"],
74
+ defaults: [
75
+ "id",
76
+ "name",
77
+ "color",
78
+ "description",
79
+ "team.key",
80
+ "parent.name",
81
+ "createdAt",
82
+ "updatedAt"
83
+ ],
84
+ full: "*"
85
+ },
86
+ state: {
87
+ ids: ["id"],
88
+ defaults: [
89
+ "id",
90
+ "name",
91
+ "type",
92
+ "color",
93
+ "position",
94
+ "team.key",
95
+ "description",
96
+ "createdAt"
97
+ ],
98
+ full: "*"
99
+ },
100
+ user: {
101
+ ids: ["id"],
102
+ defaults: [
103
+ "id",
104
+ "name",
105
+ "email",
106
+ "displayName",
107
+ "admin",
108
+ "isMe",
109
+ "active",
110
+ "avatarUrl"
111
+ ],
112
+ full: "*"
113
+ }
114
+ };
115
+ //#endregion
116
+ export { FIELD_PRESETS };
@@ -0,0 +1,282 @@
1
+ import { LinearAgentError } from "../errors/error.js";
2
+ import { FIELD_PRESETS } from "./presets.js";
3
+ //#region src/core/projection/project.ts
4
+ /**
5
+ * Field projection — parses the `--fields` flag and projects an arbitrary
6
+ * value tree to the requested dot-paths.
7
+ *
8
+ * Two-stage pipeline (KRN-08, ISS-01):
9
+ * 1. parseFields(input, entity) → `ProjectionSpec` (preset paths or sentinel)
10
+ * - Preset names ("ids" / "defaults" / "full") resolve to the entity's
11
+ * preset entry.
12
+ * - Custom CSV ("id,title,state.name") is split, trimmed, and validated
13
+ * against the entity's allowed-field registry. Unknown paths throw
14
+ * `INVALID_FIELD` (exit 2).
15
+ * 2. project(value, spec) → projected value
16
+ * - For `FULL_PRESET`, returns the input unchanged (deep-clone not
17
+ * required; identity is preserved).
18
+ * - For a path array, walks each dot-path and copies the leaf into a
19
+ * fresh object tree. Missing nested keys yield `null` rather than
20
+ * throwing — agents reading the envelope can rely on "key present"
21
+ * being a stable contract independent of upstream data shape.
22
+ *
23
+ * Allowed-field registry: a per-entity `Set<string>` of dot-paths. Phase 1
24
+ * only registers the `issue` entity; Phase 2 extends this to every curated
25
+ * read command. The registry is the contract for what `--fields` accepts —
26
+ * widening it requires a deliberate code change + a test.
27
+ *
28
+ * Threat model (T-01-25, T-01-26):
29
+ * - Custom field paths are untrusted input; the registry gates them.
30
+ * - Filter values surfaced in `INVALID_FIELD` details are the user's own
31
+ * CSV input (not secrets) — kernel redactor still scrubs token-shaped
32
+ * substrings as defense in depth.
33
+ */
34
+ /**
35
+ * Allowed dot-paths per entity. Custom `--fields` CSV values are validated
36
+ * against this registry; unknown paths trigger `INVALID_FIELD`.
37
+ *
38
+ * The list is intentionally generous (we'd rather accept a field and get
39
+ * `null` from `project()` than reject a perfectly valid SDK field). Phase 2
40
+ * expands this as new commands ship; Phase 3's raw GraphQL passthrough side-
41
+ * steps the registry entirely.
42
+ */
43
+ const ALLOWED_FIELDS = {
44
+ issue: new Set([
45
+ "id",
46
+ "identifier",
47
+ "title",
48
+ "description",
49
+ "priority",
50
+ "priorityLabel",
51
+ "estimate",
52
+ "sortOrder",
53
+ "createdAt",
54
+ "updatedAt",
55
+ "archivedAt",
56
+ "completedAt",
57
+ "startedAt",
58
+ "canceledAt",
59
+ "dueDate",
60
+ "snoozedUntilAt",
61
+ "snippet",
62
+ "state.id",
63
+ "state.name",
64
+ "state.type",
65
+ "assignee.id",
66
+ "assignee.email",
67
+ "assignee.name",
68
+ "team.id",
69
+ "team.key",
70
+ "team.name",
71
+ "project.id",
72
+ "project.name",
73
+ "cycle.id",
74
+ "cycle.number",
75
+ "parent.id",
76
+ "parent.identifier",
77
+ "url"
78
+ ]),
79
+ comment: new Set([
80
+ "id",
81
+ "body",
82
+ "bodyData",
83
+ "editedAt",
84
+ "createdAt",
85
+ "updatedAt",
86
+ "archivedAt",
87
+ "url",
88
+ "user.id",
89
+ "user.email",
90
+ "user.name",
91
+ "user.displayName",
92
+ "issue.id",
93
+ "issue.identifier",
94
+ "issue.title",
95
+ "parent.id",
96
+ "reactions"
97
+ ]),
98
+ project: new Set([
99
+ "id",
100
+ "name",
101
+ "description",
102
+ "state",
103
+ "progress",
104
+ "sortOrder",
105
+ "startDate",
106
+ "targetDate",
107
+ "startedAt",
108
+ "completedAt",
109
+ "canceledAt",
110
+ "archivedAt",
111
+ "createdAt",
112
+ "updatedAt",
113
+ "color",
114
+ "icon",
115
+ "slugId",
116
+ "url",
117
+ "lead.id",
118
+ "lead.email",
119
+ "lead.name",
120
+ "creator.id",
121
+ "creator.email",
122
+ "creator.name"
123
+ ]),
124
+ cycle: new Set([
125
+ "id",
126
+ "number",
127
+ "name",
128
+ "description",
129
+ "startsAt",
130
+ "endsAt",
131
+ "completedAt",
132
+ "progress",
133
+ "isActive",
134
+ "isPast",
135
+ "isFuture",
136
+ "isNext",
137
+ "isPrevious",
138
+ "createdAt",
139
+ "updatedAt",
140
+ "archivedAt",
141
+ "team.id",
142
+ "team.key",
143
+ "team.name"
144
+ ]),
145
+ team: new Set([
146
+ "id",
147
+ "key",
148
+ "name",
149
+ "description",
150
+ "color",
151
+ "icon",
152
+ "private",
153
+ "cycleEnabled",
154
+ "cycleDuration",
155
+ "cycleStartDay",
156
+ "cycleCooldownTime",
157
+ "cycleIssueAutoAssignStarted",
158
+ "cycleIssueAutoAssignCompleted",
159
+ "cycleLockToActive",
160
+ "createdAt",
161
+ "updatedAt",
162
+ "archivedAt",
163
+ "inviteHash",
164
+ "timezone",
165
+ "autoArchivePeriod",
166
+ "autoClosePeriod",
167
+ "defaultIssueEstimate",
168
+ "issueOrderingNoPriorityFirst",
169
+ "issueSortOrderDefaultToBottom"
170
+ ]),
171
+ label: new Set([
172
+ "id",
173
+ "name",
174
+ "color",
175
+ "description",
176
+ "createdAt",
177
+ "updatedAt",
178
+ "archivedAt",
179
+ "team.id",
180
+ "team.key",
181
+ "team.name",
182
+ "parent.id",
183
+ "parent.name",
184
+ "creator.id",
185
+ "creator.email",
186
+ "creator.name"
187
+ ]),
188
+ state: new Set([
189
+ "id",
190
+ "name",
191
+ "type",
192
+ "color",
193
+ "position",
194
+ "description",
195
+ "createdAt",
196
+ "updatedAt",
197
+ "archivedAt",
198
+ "team.id",
199
+ "team.key",
200
+ "team.name"
201
+ ]),
202
+ user: new Set([
203
+ "id",
204
+ "name",
205
+ "email",
206
+ "displayName",
207
+ "admin",
208
+ "isMe",
209
+ "active",
210
+ "guest",
211
+ "avatarUrl",
212
+ "avatarBackgroundColor",
213
+ "description",
214
+ "statusEmoji",
215
+ "statusLabel",
216
+ "statusUntilAt",
217
+ "timezone",
218
+ "createdAt",
219
+ "updatedAt",
220
+ "archivedAt",
221
+ "lastSeen",
222
+ "inviteHash",
223
+ "url"
224
+ ])
225
+ };
226
+ function parseFields(input, entity) {
227
+ const normalized = input.trim();
228
+ const presets = FIELD_PRESETS[entity];
229
+ if (normalized === "ids") return presets.ids;
230
+ if (normalized === "defaults") return presets.defaults;
231
+ if (normalized === "full") return "*";
232
+ const paths = normalized.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
233
+ const allowed = ALLOWED_FIELDS[entity];
234
+ const unknown = paths.filter((p) => !allowed.has(p));
235
+ if (unknown.length > 0) throw new LinearAgentError({
236
+ code: "INVALID_FIELD",
237
+ message: `unknown field(s) for ${entity}: ${unknown.join(", ")}`,
238
+ details: {
239
+ entity,
240
+ unknown,
241
+ allowed: [...allowed].sort()
242
+ }
243
+ });
244
+ return paths;
245
+ }
246
+ /**
247
+ * Walk `value` and pull out only the requested dot-paths. For each leaf:
248
+ * - If the upstream key exists, copy the value (no clone — references
249
+ * into the SDK shape are fine because we never mutate the result).
250
+ * - If the upstream key is missing at any level, the leaf is `null`.
251
+ *
252
+ * For `FULL_PRESET`, returns the input unchanged.
253
+ */
254
+ function project(value, spec) {
255
+ if (spec === "*") return value;
256
+ if (spec.length === 0) return {};
257
+ const out = {};
258
+ for (const path of spec) {
259
+ const parts = path.split(".");
260
+ let src = value;
261
+ let dst = out;
262
+ for (let i = 0; i < parts.length; i++) {
263
+ const key = parts[i];
264
+ const isLeaf = i === parts.length - 1;
265
+ const srcValue = readKey(src, key);
266
+ if (isLeaf) dst[key] = srcValue ?? null;
267
+ else {
268
+ if (!(key in dst) || typeof dst[key] !== "object" || dst[key] === null) dst[key] = {};
269
+ dst = dst[key];
270
+ src = srcValue;
271
+ }
272
+ }
273
+ }
274
+ return out;
275
+ }
276
+ function readKey(value, key) {
277
+ if (value === null || typeof value !== "object") return void 0;
278
+ if (!(key in value)) return void 0;
279
+ return value[key];
280
+ }
281
+ //#endregion
282
+ export { parseFields, project };
@@ -0,0 +1,45 @@
1
+ //#region src/core/redact/redact.ts
2
+ /**
3
+ * Token redactor — last-line-of-defense scrubber for Linear PATs.
4
+ *
5
+ * The kernel's `format()` function in `src/core/output/format.ts` calls
6
+ * `redact()` on every envelope before stringifying it. Property-based
7
+ * tests in `test/core/redact.test.ts` (≥200 runs) prove no
8
+ * `lin_api_*` or `lin_oauth_*` substring can survive a round-trip.
9
+ *
10
+ * Why the regex is permissive on length: Linear PATs are currently 40-ish
11
+ * base64url chars after the prefix, but the public format isn't versioned.
12
+ * `[A-Za-z0-9_-]+` matches greedily on token-shaped suffixes — slightly
13
+ * over-aggressive on otherwise-innocuous strings is the right tradeoff for
14
+ * a security boundary (PITFALLS § Pitfalls 3, 4).
15
+ */
16
+ const REDACTED = "[REDACTED]";
17
+ const TOKEN_PATTERN = /lin_(?:api|oauth)_[A-Za-z0-9_-]+/g;
18
+ const CIRCULAR_SENTINEL = "[CIRCULAR]";
19
+ /**
20
+ * Walk an arbitrary value and replace every Linear-PAT-shaped substring
21
+ * inside any string field with `[REDACTED]`. Returns a new value (does
22
+ * not mutate the input). Cycle-safe: revisited objects/arrays return the
23
+ * `[CIRCULAR]` sentinel so the walk is bounded (T-01-04 in the threat
24
+ * model).
25
+ *
26
+ * Type parameter `T` is preserved as a return-type hint, but the runtime
27
+ * value MAY differ from `T` if the input contains cycles (the `[CIRCULAR]`
28
+ * sentinel is a string, not the original object). Callers feeding cyclic
29
+ * inputs should not rely on the static return type past one level.
30
+ */
31
+ function redact(input) {
32
+ return walk(input, /* @__PURE__ */ new WeakSet());
33
+ }
34
+ function walk(value, seen) {
35
+ if (typeof value === "string") return value.replace(TOKEN_PATTERN, REDACTED);
36
+ if (value === null || typeof value !== "object") return value;
37
+ if (seen.has(value)) return CIRCULAR_SENTINEL;
38
+ seen.add(value);
39
+ if (Array.isArray(value)) return value.map((v) => walk(v, seen));
40
+ const out = {};
41
+ for (const [k, v] of Object.entries(value)) out[k] = walk(v, seen);
42
+ return out;
43
+ }
44
+ //#endregion
45
+ export { redact };
@@ -0,0 +1,60 @@
1
+ import { LinearAgentError } from "../errors/error.js";
2
+ import { withRateLimitRetry } from "../transport/rate-limit.js";
3
+ import { UUID_RE } from "../../lib/filter-heuristics.js";
4
+ //#region src/core/resolvers/cycle.ts
5
+ const cache = /* @__PURE__ */ new Map();
6
+ /** Matches `+N`, `-N`, and bare `N` integer offsets like `0`, `+1`, `-3`. */
7
+ const OFFSET_RE = /^[+-]?\d+$/;
8
+ /**
9
+ * Resolve a cycle reference (UUID, `current`, `next`, `previous`, `+N`, `-N`,
10
+ * `0`, or cycle name) to a cycle UUID, scoped to one `${workspace}:${teamId}`
11
+ * pair.
12
+ */
13
+ async function resolveCycleId(client, workspaceName, teamId, ref, retryOpts) {
14
+ if (UUID_RE.test(ref)) return ref;
15
+ const key = `${workspaceName}:${teamId}`;
16
+ let cyclesP = cache.get(key);
17
+ if (!cyclesP) {
18
+ cyclesP = (async () => {
19
+ const team = await withRateLimitRetry(() => client.team(teamId), retryOpts);
20
+ return (await withRateLimitRetry(() => team.cycles({ first: 250 }), retryOpts)).nodes.map((c) => ({
21
+ id: c.id,
22
+ number: c.number,
23
+ name: c.name ?? null,
24
+ isActive: c.isActive ?? false
25
+ })).sort((a, b) => a.number - b.number);
26
+ })();
27
+ cache.set(key, cyclesP);
28
+ }
29
+ let cycles;
30
+ try {
31
+ cycles = await cyclesP;
32
+ } catch (e) {
33
+ cache.delete(key);
34
+ throw e;
35
+ }
36
+ const activeIdx = cycles.findIndex((c) => c.isActive);
37
+ const findOffset = (delta) => {
38
+ if (activeIdx === -1) return void 0;
39
+ return cycles[activeIdx + delta];
40
+ };
41
+ let resolved;
42
+ if (ref === "current") resolved = activeIdx >= 0 ? cycles[activeIdx] : void 0;
43
+ else if (ref === "next") resolved = findOffset(1);
44
+ else if (ref === "previous") resolved = findOffset(-1);
45
+ else if (OFFSET_RE.test(ref)) resolved = findOffset(Number.parseInt(ref, 10));
46
+ else resolved = cycles.find((c) => c.name?.toLowerCase() === ref.toLowerCase());
47
+ if (!resolved) throw new LinearAgentError({
48
+ code: "CYCLE_NOT_FOUND",
49
+ message: `cycle not found: ${ref}`,
50
+ details: {
51
+ teamId,
52
+ requested: ref,
53
+ availableNumbers: cycles.map((c) => c.number),
54
+ activeNumber: activeIdx >= 0 ? cycles[activeIdx]?.number ?? null : null
55
+ }
56
+ });
57
+ return resolved.id;
58
+ }
59
+ //#endregion
60
+ export { resolveCycleId };
@@ -0,0 +1,7 @@
1
+ import "./cycle.js";
2
+ import "./label.js";
3
+ import "./project.js";
4
+ import "./project-status.js";
5
+ import "./state.js";
6
+ import "./team.js";
7
+ export {};
@@ -0,0 +1,54 @@
1
+ import { LinearAgentError } from "../errors/error.js";
2
+ import { withRateLimitRetry } from "../transport/rate-limit.js";
3
+ import { UUID_RE } from "../../lib/filter-heuristics.js";
4
+ //#region src/core/resolvers/label.ts
5
+ const cache = /* @__PURE__ */ new Map();
6
+ /**
7
+ * Resolve a label name (or UUID) to a label UUID, scoped to one
8
+ * `${workspace}:${teamId}` pair.
9
+ */
10
+ async function resolveLabelId(client, workspaceName, teamId, nameOrId, retryOpts) {
11
+ if (UUID_RE.test(nameOrId)) return nameOrId;
12
+ const key = `${workspaceName}:${teamId}`;
13
+ let map = cache.get(key);
14
+ if (!map) {
15
+ map = (async () => {
16
+ const conn = await withRateLimitRetry(() => client.issueLabels({
17
+ filter: { team: { id: { eq: teamId } } },
18
+ first: 250
19
+ }), retryOpts);
20
+ const m = /* @__PURE__ */ new Map();
21
+ for (const l of conn.nodes) m.set(l.name.toLowerCase(), l.id);
22
+ return m;
23
+ })();
24
+ cache.set(key, map);
25
+ }
26
+ let m;
27
+ try {
28
+ m = await map;
29
+ } catch (e) {
30
+ cache.delete(key);
31
+ throw e;
32
+ }
33
+ const id = m.get(nameOrId.toLowerCase());
34
+ if (!id) throw new LinearAgentError({
35
+ code: "LABEL_NOT_FOUND",
36
+ message: `label not found: ${nameOrId}`,
37
+ details: {
38
+ teamId,
39
+ requested: nameOrId,
40
+ available: [...m.keys()].sort()
41
+ }
42
+ });
43
+ return id;
44
+ }
45
+ /**
46
+ * Resolve a list of label names/UUIDs to label UUIDs in input order. Shares
47
+ * the underlying per-team cache so a list of N names triggers at most one SDK
48
+ * call (subsequent name lookups read from the cached map; UUIDs pass through).
49
+ */
50
+ async function resolveLabelIds(client, workspaceName, teamId, namesOrIds, retryOpts) {
51
+ return Promise.all(namesOrIds.map((n) => resolveLabelId(client, workspaceName, teamId, n, retryOpts)));
52
+ }
53
+ //#endregion
54
+ export { resolveLabelId, resolveLabelIds };