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.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/bin/run.js +4 -0
- package/dist/commands/comment/create.js +94 -0
- package/dist/commands/comment/delete.js +74 -0
- package/dist/commands/comment/list.js +84 -0
- package/dist/commands/comment/update.js +80 -0
- package/dist/commands/cycle/current.js +78 -0
- package/dist/commands/cycle/list.js +84 -0
- package/dist/commands/cycle/move.js +91 -0
- package/dist/commands/describe.js +65 -0
- package/dist/commands/graphql/index.js +92 -0
- package/dist/commands/install-skill.js +54 -0
- package/dist/commands/issue/archive.js +75 -0
- package/dist/commands/issue/create.js +115 -0
- package/dist/commands/issue/get.js +84 -0
- package/dist/commands/issue/list.js +93 -0
- package/dist/commands/issue/purge.js +81 -0
- package/dist/commands/issue/search.js +109 -0
- package/dist/commands/issue/transition.js +91 -0
- package/dist/commands/issue/trash.js +75 -0
- package/dist/commands/issue/update.js +126 -0
- package/dist/commands/label/create.js +91 -0
- package/dist/commands/label/list.js +76 -0
- package/dist/commands/list-tools.js +47 -0
- package/dist/commands/me.js +71 -0
- package/dist/commands/project/create.js +101 -0
- package/dist/commands/project/get.js +83 -0
- package/dist/commands/project/list.js +75 -0
- package/dist/commands/project/update-status.js +99 -0
- package/dist/commands/project/update.js +99 -0
- package/dist/commands/raw/batch.js +85 -0
- package/dist/commands/raw/index.js +72 -0
- package/dist/commands/schema.js +69 -0
- package/dist/commands/state/list.js +77 -0
- package/dist/commands/team/get.js +73 -0
- package/dist/commands/team/list.js +73 -0
- package/dist/commands/whoami.js +71 -0
- package/dist/commands/workspace/add.js +97 -0
- package/dist/commands/workspace/list.js +47 -0
- package/dist/commands/workspace/remove.js +63 -0
- package/dist/commands/workspace/replace-token.js +89 -0
- package/dist/commands/workspace/use.js +54 -0
- package/dist/core/client/factory.js +28 -0
- package/dist/core/client/index.js +2 -0
- package/dist/core/config/index.js +4 -0
- package/dist/core/config/paths.js +30 -0
- package/dist/core/config/schema.js +36 -0
- package/dist/core/config/store.js +149 -0
- package/dist/core/errors/error.js +142 -0
- package/dist/core/errors/exit-codes.js +70 -0
- package/dist/core/output/envelope.js +53 -0
- package/dist/core/output/format.js +42 -0
- package/dist/core/output/index.js +3 -0
- package/dist/core/pagination/flags.js +29 -0
- package/dist/core/pagination/index.js +2 -0
- package/dist/core/projection/presets.js +116 -0
- package/dist/core/projection/project.js +282 -0
- package/dist/core/redact/redact.js +45 -0
- package/dist/core/resolvers/cycle.js +60 -0
- package/dist/core/resolvers/index.js +7 -0
- package/dist/core/resolvers/label.js +54 -0
- package/dist/core/resolvers/project-status.js +42 -0
- package/dist/core/resolvers/project.js +43 -0
- package/dist/core/resolvers/state.js +46 -0
- package/dist/core/resolvers/team.js +50 -0
- package/dist/core/transport/fetch-interceptor.js +109 -0
- package/dist/core/transport/index.js +3 -0
- package/dist/core/transport/rate-limit.js +167 -0
- package/dist/core/workspace/resolver.js +70 -0
- package/dist/core/workspace/write-guard.js +43 -0
- package/dist/generated/graphql.js +89428 -0
- package/dist/generated/operations.js +3013 -0
- package/dist/lib/comment-create-runtime.js +96 -0
- package/dist/lib/comment-delete-runtime.js +46 -0
- package/dist/lib/comment-list-runtime.js +182 -0
- package/dist/lib/comment-update-runtime.js +93 -0
- package/dist/lib/cycle-current-runtime.js +90 -0
- package/dist/lib/cycle-list-runtime.js +151 -0
- package/dist/lib/cycle-move-runtime.js +142 -0
- package/dist/lib/describe-runtime.js +180 -0
- package/dist/lib/filter-heuristics.js +59 -0
- package/dist/lib/graphql-runtime.js +202 -0
- package/dist/lib/include-fragments.js +73 -0
- package/dist/lib/install-skill-runtime.js +228 -0
- package/dist/lib/introspection-registry.js +488 -0
- package/dist/lib/issue-archive-runtime.js +89 -0
- package/dist/lib/issue-create-runtime.js +175 -0
- package/dist/lib/issue-get-runtime.js +153 -0
- package/dist/lib/issue-list-runtime.js +164 -0
- package/dist/lib/issue-purge-runtime.js +89 -0
- package/dist/lib/issue-search-runtime.js +114 -0
- package/dist/lib/issue-transition-runtime.js +131 -0
- package/dist/lib/issue-trash-runtime.js +84 -0
- package/dist/lib/issue-update-runtime.js +164 -0
- package/dist/lib/label-create-runtime.js +113 -0
- package/dist/lib/label-list-runtime.js +97 -0
- package/dist/lib/levenshtein.js +42 -0
- package/dist/lib/list-tools-runtime.js +38 -0
- package/dist/lib/me-runtime.js +55 -0
- package/dist/lib/project-create-runtime.js +103 -0
- package/dist/lib/project-get-runtime.js +134 -0
- package/dist/lib/project-list-runtime.js +84 -0
- package/dist/lib/project-update-runtime.js +110 -0
- package/dist/lib/project-update-status-runtime.js +91 -0
- package/dist/lib/raw-batch-runtime.js +229 -0
- package/dist/lib/raw-runtime.js +171 -0
- package/dist/lib/schema-loader.js +41 -0
- package/dist/lib/schema-runtime.js +65 -0
- package/dist/lib/state-list-runtime.js +93 -0
- package/dist/lib/team-get-runtime.js +55 -0
- package/dist/lib/team-list-runtime.js +52 -0
- package/dist/lib/workspace-runtime.js +112 -0
- package/dist/operations/_registry.zod.js +5337 -0
- package/oclif.manifest.json +3631 -0
- package/package.json +99 -0
- package/schema.graphql +30772 -0
- 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,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,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,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 };
|