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,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 };
|