@dahrk/linear 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.
@@ -0,0 +1,198 @@
1
+ /**
2
+ * The real Agent Session client over `@linear/sdk` (build spec section 14). This is the
3
+ * LIVE path - it makes authenticated GraphQL calls to Linear - so it is only constructed
4
+ * when a Connection carries a bearer token; the offline harness uses the recording client.
5
+ *
6
+ * Verified against @linear/sdk v86: `createAgentActivity({ agentSessionId, content, signal?,
7
+ * signalMetadata?, ephemeral? })` (content is an untyped JSONObject - the typed payload shapes
8
+ * are in Linear's agent-interaction docs), `updateAgentSession(id, { plan, externalUrls })`,
9
+ * `agentSession(id)` (resolves the issue), `updateIssue(id, { stateId, delegateId })`,
10
+ * `team(id).states({ filter })`, `viewer`, and `createAttachment({ issueId, url, title, ... })`.
11
+ * There is no app-settable session `state`: Linear derives it from activities and signals, so
12
+ * `setState` is a no-op here.
13
+ */
14
+ import { AgentActivitySignal, LinearClient } from "@linear/sdk";
15
+ import { filterExternalUrls } from "./index.js";
16
+ /** Build the (untyped) activity content payload Linear expects for each activity type. */
17
+ function contentOf(a) {
18
+ switch (a.type) {
19
+ case "thought":
20
+ return { type: "thought", body: a.text };
21
+ case "response":
22
+ return { type: "response", body: a.text };
23
+ case "error":
24
+ return { type: "error", body: a.text };
25
+ case "action":
26
+ return { type: "action", action: a.tool ?? "tool", parameter: a.text };
27
+ }
28
+ }
29
+ /** Normalise an elicitation option into Linear's `{ label?, value }` shape. */
30
+ function optionOf(o) {
31
+ return typeof o === "string" ? { value: o } : { ...(o.label ? { label: o.label } : {}), value: o.value };
32
+ }
33
+ export function createLinearClient(token) {
34
+ const client = new LinearClient({ accessToken: token });
35
+ return {
36
+ async postActivity(sessionId, activity) {
37
+ await client.createAgentActivity({
38
+ agentSessionId: sessionId,
39
+ content: contentOf(activity),
40
+ // Only thought/action may be ephemeral; the caller guarantees this, Linear ignores it otherwise.
41
+ ...(activity.ephemeral ? { ephemeral: true } : {}),
42
+ });
43
+ },
44
+ async raiseElicitation(sessionId, prompt, options) {
45
+ await client.createAgentActivity({
46
+ agentSessionId: sessionId,
47
+ content: { type: "elicitation", body: prompt },
48
+ signal: AgentActivitySignal.Select,
49
+ ...(options ? { signalMetadata: { options: options.map(optionOf) } } : {}),
50
+ });
51
+ },
52
+ async suggestRepositories(issueId, candidates) {
53
+ // Linear ranks the candidates we already have access to; we keep its order and never re-sort by
54
+ // confidence ourselves (ordering only - the human picks). `hostname` is required by the API.
55
+ const payload = await client.issueRepositorySuggestions(candidates.map((c) => ({ hostname: c.hostname ?? "", repositoryFullName: c.repositoryFullName })), issueId);
56
+ return payload.suggestions.map((s) => ({
57
+ repositoryFullName: s.repositoryFullName,
58
+ ...(s.hostname ? { hostname: s.hostname } : {}),
59
+ confidence: s.confidence,
60
+ }));
61
+ },
62
+ async addIssueLabel(issueId, name) {
63
+ // Get-or-create the label by name (issueAddLabel needs an id), then attach it. Idempotent:
64
+ // createIssueLabel only runs when the name is absent, and issueAddLabel no-ops on a label the
65
+ // issue already carries, so a repeat stamp is a safe no-op.
66
+ const labels = await client.issueLabels();
67
+ while (labels.pageInfo.hasNextPage)
68
+ await labels.fetchNext();
69
+ let labelId = labels.nodes.find((l) => l.name === name)?.id;
70
+ if (!labelId) {
71
+ const created = await client.createIssueLabel({ name });
72
+ labelId = (await created.issueLabel)?.id;
73
+ }
74
+ if (labelId)
75
+ await client.issueAddLabel(issueId, labelId);
76
+ },
77
+ async requestAuth(sessionId, prompt, auth) {
78
+ // The `auth` signal renders Linear's native account-linking UI from an elicitation; the user
79
+ // completes the link at `auth.url`, then the agent resumes on the follow-up `prompted` webhook.
80
+ await client.createAgentActivity({
81
+ agentSessionId: sessionId,
82
+ content: { type: "elicitation", body: prompt },
83
+ signal: AgentActivitySignal.Auth,
84
+ signalMetadata: {
85
+ url: auth.url,
86
+ ...(auth.userId ? { userId: auth.userId } : {}),
87
+ ...(auth.providerName ? { providerName: auth.providerName } : {}),
88
+ },
89
+ });
90
+ },
91
+ async startIssue(sessionId) {
92
+ // Resolve the issue from the session itself, so no issue id has to be threaded through the engine.
93
+ const session = await client.agentSession(sessionId);
94
+ const issue = await session.issue;
95
+ if (!issue)
96
+ return;
97
+ // Self-delegate (idempotent: assignment already sets the app as delegate; @mention does not).
98
+ const me = await client.viewer;
99
+ const update = { delegateId: me.id };
100
+ // Move to the team's first `started` state, but only from a not-yet-started status.
101
+ const current = await issue.state;
102
+ if (current && current.type !== "started" && current.type !== "completed" && current.type !== "canceled") {
103
+ const team = await issue.team;
104
+ if (team) {
105
+ const states = await team.states({ filter: { type: { eq: "started" } } });
106
+ const first = [...states.nodes].sort((a, b) => a.position - b.position)[0];
107
+ if (first)
108
+ update.stateId = first.id;
109
+ }
110
+ }
111
+ await client.updateIssue(issue.id, update);
112
+ },
113
+ async moveIssueToReview(sessionId) {
114
+ const session = await client.agentSession(sessionId);
115
+ const issue = await session.issue;
116
+ if (!issue)
117
+ return;
118
+ // Never disturb a closed issue.
119
+ const current = await issue.state;
120
+ if (current && (current.type === "completed" || current.type === "canceled"))
121
+ return;
122
+ const team = await issue.team;
123
+ if (!team)
124
+ return;
125
+ // Linear has no "review" state TYPE, so match by name. Prefer an explicit configured name,
126
+ // else the first state whose name looks review-ish. No match = leave the issue in `started`
127
+ // (never auto-complete), so a human keeps iterating and can re-summon @Skakel on the same PR.
128
+ const wanted = (process.env.SKAKEL_REVIEW_STATE_NAME ?? "").trim().toLowerCase();
129
+ const states = [...(await team.states()).nodes].sort((a, b) => a.position - b.position);
130
+ const pick = wanted
131
+ ? states.find((s) => s.name.toLowerCase() === wanted)
132
+ : states.find((s) => /review|in review|qa/i.test(s.name));
133
+ if (!pick || (current && pick.id === current.id))
134
+ return;
135
+ await client.updateIssue(issue.id, { stateId: pick.id });
136
+ },
137
+ async commentOnIssue(sessionId, body) {
138
+ // A normal top-level issue comment (not an agent activity), so a run summary lands in the
139
+ // issue's comment thread where a human reads it, rather than only in the agent session panel.
140
+ const session = await client.agentSession(sessionId);
141
+ const issue = await session.issue;
142
+ if (!issue)
143
+ return;
144
+ await client.createComment({ issueId: issue.id, body });
145
+ },
146
+ async attachPr(sessionId, pr) {
147
+ // Idempotent by URL: Linear's `attachmentCreate` upserts on (issueId, url) - a second call with
148
+ // the same `pr.url` on the same issue UPDATES the existing attachment rather than creating a
149
+ // duplicate. This is what lets the host loop retry `open-pr` (e.g. after a crash/replay) without
150
+ // littering the issue with duplicate PR attachments; the reporter relies on the same invariant.
151
+ const session = await client.agentSession(sessionId);
152
+ const issue = await session.issue;
153
+ if (!issue)
154
+ return;
155
+ await client.createAttachment({
156
+ issueId: issue.id,
157
+ url: pr.url,
158
+ title: pr.title,
159
+ ...(pr.subtitle ? { subtitle: pr.subtitle } : {}),
160
+ ...(pr.iconUrl ? { iconUrl: pr.iconUrl } : {}),
161
+ });
162
+ },
163
+ async createDocument(sessionId, doc) {
164
+ // Resolve the issue from the session so no issue id has to be threaded through the engine,
165
+ // mirroring attachPr. `documentCreate` with an issueId links the document to the issue.
166
+ const session = await client.agentSession(sessionId);
167
+ const issue = await session.issue;
168
+ const payload = await client.createDocument({
169
+ title: doc.title,
170
+ content: doc.content,
171
+ ...(issue ? { issueId: issue.id } : {}),
172
+ });
173
+ const created = await payload.document;
174
+ return { id: created?.id ?? payload.documentId ?? "", url: created?.url ?? "" };
175
+ },
176
+ async setPlan(sessionId, items) {
177
+ // Linear expects the plan as a full array of { content, status } steps (replaced wholesale),
178
+ // NOT a wrapper object. Our PlanItem.status values (pending|inProgress|completed|canceled)
179
+ // already match Linear's enum; show the human title (falling back to the stage id) as content.
180
+ await client.updateAgentSession(sessionId, {
181
+ plan: items.map((i) => ({ content: i.title ?? i.stageId, status: i.status })),
182
+ });
183
+ },
184
+ async setExternalUrls(sessionId, urls) {
185
+ // Linear rejects non-http(s) URLs (e.g. internal skakel:// run refs) and requires each URL to be
186
+ // unique; filterExternalUrls drops the rest. The array is replaced wholesale, so no-op on empty
187
+ // rather than clobber any existing links with nothing.
188
+ const valid = filterExternalUrls(urls);
189
+ if (valid.length === 0)
190
+ return;
191
+ await client.updateAgentSession(sessionId, { externalUrls: valid });
192
+ },
193
+ async setState() {
194
+ // Linear derives the agent-session state from activities/signals; not app-settable.
195
+ },
196
+ };
197
+ }
198
+ //# sourceMappingURL=linear-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-client.js","sourceRoot":"","sources":["../src/linear-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAWhD,0FAA0F;AAC1F,SAAS,SAAS,CAAC,CAAW;IAC5B,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,SAAS;YACZ,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC3C,KAAK,UAAU;YACb,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC5C,KAAK,OAAO;YACV,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACzC,KAAK,QAAQ;YACX,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,IAAI,IAAI,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC3E,CAAC;AACH,CAAC;AAED,+EAA+E;AAC/E,SAAS,QAAQ,CAAC,CAAe;IAC/B,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;AAC3G,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;IACxD,OAAO;QACL,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ;YACpC,MAAM,MAAM,CAAC,mBAAmB,CAAC;gBAC/B,cAAc,EAAE,SAAS;gBACzB,OAAO,EAAE,SAAS,CAAC,QAAQ,CAAC;gBAC5B,iGAAiG;gBACjG,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACnD,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO;YAC/C,MAAM,MAAM,CAAC,mBAAmB,CAAC;gBAC/B,cAAc,EAAE,SAAS;gBACzB,OAAO,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE;gBAC9C,MAAM,EAAE,mBAAmB,CAAC,MAAM;gBAClC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3E,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,mBAAmB,CAAC,OAAO,EAAE,UAAU;YAC3C,gGAAgG;YAChG,6FAA6F;YAC7F,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,0BAA0B,CACrD,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC,kBAAkB,EAAE,CAAC,CAAC,EACjG,OAAO,CACR,CAAC;YACF,OAAO,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACrC,kBAAkB,EAAE,CAAC,CAAC,kBAAkB;gBACxC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,UAAU,EAAE,CAAC,CAAC,UAAU;aACzB,CAAC,CAAC,CAAC;QACN,CAAC;QACD,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI;YAC/B,2FAA2F;YAC3F,8FAA8F;YAC9F,4DAA4D;YAC5D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;YAC1C,OAAO,MAAM,CAAC,QAAQ,CAAC,WAAW;gBAAE,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;YAC7D,IAAI,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;YAC5D,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;gBACxD,OAAO,GAAG,CAAC,MAAM,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,CAAC;YAC3C,CAAC;YACD,IAAI,OAAO;gBAAE,MAAM,MAAM,CAAC,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC5D,CAAC;QACD,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI;YACvC,6FAA6F;YAC7F,gGAAgG;YAChG,MAAM,MAAM,CAAC,mBAAmB,CAAC;gBAC/B,cAAc,EAAE,SAAS;gBACzB,OAAO,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE;gBAC9C,MAAM,EAAE,mBAAmB,CAAC,IAAI;gBAChC,cAAc,EAAE;oBACd,GAAG,EAAE,IAAI,CAAC,GAAG;oBACb,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAC/C,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBAClE;aACF,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,SAAS;YACxB,mGAAmG;YACnG,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,OAAO;YACnB,8FAA8F;YAC9F,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;YAC/B,MAAM,MAAM,GAA8C,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAChF,oFAAoF;YACpF,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC;YAClC,IAAI,OAAO,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,KAAK,WAAW,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;gBACzG,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC;gBAC9B,IAAI,IAAI,EAAE,CAAC;oBACT,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC;oBAC1E,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;oBAC3E,IAAI,KAAK;wBAAE,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,EAAE,CAAC;gBACvC,CAAC;YACH,CAAC;YACD,MAAM,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QACD,KAAK,CAAC,iBAAiB,CAAC,SAAS;YAC/B,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,OAAO;YACnB,gCAAgC;YAChC,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,KAAK,CAAC;YAClC,IAAI,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,IAAI,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC;gBAAE,OAAO;YACrF,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC;YAC9B,IAAI,CAAC,IAAI;gBAAE,OAAO;YAClB,2FAA2F;YAC3F,4FAA4F;YAC5F,8FAA8F;YAC9F,MAAM,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACjF,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;YACxF,MAAM,IAAI,GAAG,MAAM;gBACjB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC;gBACrD,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;YAC5D,IAAI,CAAC,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE,CAAC;gBAAE,OAAO;YACzD,MAAM,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC3D,CAAC;QACD,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI;YAClC,0FAA0F;YAC1F,8FAA8F;YAC9F,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,OAAO;YACnB,MAAM,MAAM,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAgB;YACxC,gGAAgG;YAChG,6FAA6F;YAC7F,iGAAiG;YACjG,gGAAgG;YAChG,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;YAClC,IAAI,CAAC,KAAK;gBAAE,OAAO;YACnB,MAAM,MAAM,CAAC,gBAAgB,CAAC;gBAC5B,OAAO,EAAE,KAAK,CAAC,EAAE;gBACjB,GAAG,EAAE,EAAE,CAAC,GAAG;gBACX,KAAK,EAAE,EAAE,CAAC,KAAK;gBACf,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,GAAG,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC/C,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,cAAc,CAAC,SAAS,EAAE,GAAkB;YAChD,2FAA2F;YAC3F,wFAAwF;YACxF,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC;YAClC,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC;gBAC1C,KAAK,EAAE,GAAG,CAAC,KAAK;gBAChB,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACxC,CAAC,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,QAAQ,CAAC;YACvC,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,OAAO,CAAC,UAAU,IAAI,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC;QAClF,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK;YAC5B,6FAA6F;YAC7F,2FAA2F;YAC3F,+FAA+F;YAC/F,MAAM,MAAM,CAAC,kBAAkB,CAAC,SAAS,EAAE;gBACzC,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;aAC9E,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,eAAe,CAAC,SAAS,EAAE,IAAI;YACnC,iGAAiG;YACjG,gGAAgG;YAChG,uDAAuD;YACvD,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAC/B,MAAM,MAAM,CAAC,kBAAkB,CAAC,SAAS,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;QACD,KAAK,CAAC,QAAQ;YACZ,oFAAoF;QACtF,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Linear OAuth 2.0 helpers for the agent install + token rotation (build spec section 14).
3
+ *
4
+ * An agent becomes assignable/mentionable in a workspace only while the app is INSTALLED via
5
+ * the authorization-code flow with `actor=app` and the `app:assignable` / `app:mentionable`
6
+ * scopes (admin consent). That flow also yields the access + refresh tokens the hub posts with.
7
+ * Access tokens last ~24h, so the hub refreshes them with the refresh token (see the hub's
8
+ * `resolveClient`). Plain `fetch`, no SDK; cyrus is a reference, not a dependency.
9
+ */
10
+ /** The agent scopes: read/write plus the two that make the app assignable + mentionable. */
11
+ export declare const DEFAULT_AGENT_SCOPES: readonly ["read", "write", "app:assignable", "app:mentionable"];
12
+ export interface LinearTokens {
13
+ accessToken: string;
14
+ /** Absent on some refresh responses; callers keep the prior refresh token when so. */
15
+ refreshToken?: string;
16
+ /** When the access token expires (computed from `expires_in`). */
17
+ expiresAt: Date;
18
+ scope?: string;
19
+ }
20
+ /**
21
+ * Build the Linear consent URL. The admin opens this; approving installs the agent (with
22
+ * `actor=app`) and redirects to `redirectUri?code=...&state=...`. `state` is opaque here - the
23
+ * caller signs it (CSRF) and recovers the connection from it in the callback.
24
+ */
25
+ export declare function authorizeUrl(params: {
26
+ clientId: string;
27
+ redirectUri: string;
28
+ state: string;
29
+ scopes?: readonly string[];
30
+ actor?: "app" | "user";
31
+ }): string;
32
+ /** Exchange the callback `code` for access + refresh tokens (one-time, at install). */
33
+ export declare function exchangeCode(params: {
34
+ clientId: string;
35
+ clientSecret: string;
36
+ code: string;
37
+ redirectUri: string;
38
+ }, now?: () => Date): Promise<LinearTokens>;
39
+ /**
40
+ * Resolve the workspace (organization) id the access token belongs to. Used at install to backfill
41
+ * a connection's `workspaceIds` from the token, so an operator need not paste the org id by hand and
42
+ * intake can match the workspace on the very first webhook. Plain GraphQL fetch, no SDK.
43
+ */
44
+ export declare function fetchOrganizationId(accessToken: string): Promise<string>;
45
+ /** The agent identity + bound workspace a token resolves to, for the connection Probe. */
46
+ export interface LinearProbe {
47
+ viewer?: {
48
+ id: string;
49
+ name?: string;
50
+ };
51
+ organization?: {
52
+ id: string;
53
+ name?: string;
54
+ urlKey?: string;
55
+ };
56
+ }
57
+ /**
58
+ * Probe a stored access token: query `viewer` (the app/agent user) and `organization` (the bound
59
+ * workspace) so the portal can confirm a connection's token is valid and show which workspace it
60
+ * binds to. Throws on an auth failure / GraphQL error, which the caller renders as "token invalid".
61
+ */
62
+ export declare function probeToken(accessToken: string): Promise<LinearProbe>;
63
+ /** Rotate the access token using the stored refresh token (the hub calls this near expiry / on 401). */
64
+ export declare function refreshTokens(params: {
65
+ clientId: string;
66
+ clientSecret: string;
67
+ refreshToken: string;
68
+ }, now?: () => Date): Promise<LinearTokens>;
69
+ /**
70
+ * Mint an `app` actor token via the `client_credentials` grant: headless (no user, no redirect, no
71
+ * callback, no refresh token), scoped to the app's workspace, valid ~30 days. Linear's documented
72
+ * renewal is simply to mint a fresh one on a 401/expiry. Requires the app's "Client credentials"
73
+ * toggle to be enabled. This is the durable token path for a headless agent, vs the interactive
74
+ * authorization-code flow (24h token + fragile refresh + "already installed" install UX).
75
+ */
76
+ export declare function mintAppToken(params: {
77
+ clientId: string;
78
+ clientSecret: string;
79
+ scopes?: readonly string[];
80
+ }, now?: () => Date): Promise<LinearTokens>;
81
+ //# sourceMappingURL=oauth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,iEAAkE,CAAC;AAEpG,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,SAAS,EAAE,IAAI,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CACxB,GAAG,MAAM,CAWT;AA+BD,uFAAuF;AACvF,wBAAgB,YAAY,CAC1B,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EACrF,GAAG,GAAE,MAAM,IAAuB,GACjC,OAAO,CAAC,YAAY,CAAC,CAWvB;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB9E;AAED,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,YAAY,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/D;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAoB1E;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAC3B,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,EACxE,GAAG,GAAE,MAAM,IAAuB,GACjC,OAAO,CAAC,YAAY,CAAC,CAUvB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,EAC9E,GAAG,GAAE,MAAM,IAAuB,GACjC,OAAO,CAAC,YAAY,CAAC,CAUvB"}
package/dist/oauth.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Linear OAuth 2.0 helpers for the agent install + token rotation (build spec section 14).
3
+ *
4
+ * An agent becomes assignable/mentionable in a workspace only while the app is INSTALLED via
5
+ * the authorization-code flow with `actor=app` and the `app:assignable` / `app:mentionable`
6
+ * scopes (admin consent). That flow also yields the access + refresh tokens the hub posts with.
7
+ * Access tokens last ~24h, so the hub refreshes them with the refresh token (see the hub's
8
+ * `resolveClient`). Plain `fetch`, no SDK; cyrus is a reference, not a dependency.
9
+ */
10
+ const AUTHORIZE_URL = "https://linear.app/oauth/authorize";
11
+ const TOKEN_URL = "https://api.linear.app/oauth/token";
12
+ const GRAPHQL_URL = "https://api.linear.app/graphql";
13
+ /** The agent scopes: read/write plus the two that make the app assignable + mentionable. */
14
+ export const DEFAULT_AGENT_SCOPES = ["read", "write", "app:assignable", "app:mentionable"];
15
+ /**
16
+ * Build the Linear consent URL. The admin opens this; approving installs the agent (with
17
+ * `actor=app`) and redirects to `redirectUri?code=...&state=...`. `state` is opaque here - the
18
+ * caller signs it (CSRF) and recovers the connection from it in the callback.
19
+ */
20
+ export function authorizeUrl(params) {
21
+ const u = new URL(AUTHORIZE_URL);
22
+ u.searchParams.set("client_id", params.clientId);
23
+ u.searchParams.set("redirect_uri", params.redirectUri);
24
+ u.searchParams.set("response_type", "code");
25
+ u.searchParams.set("scope", (params.scopes ?? DEFAULT_AGENT_SCOPES).join(","));
26
+ u.searchParams.set("state", params.state);
27
+ u.searchParams.set("actor", params.actor ?? "app");
28
+ // Force the consent screen so re-installing always re-grants (and re-issues a refresh token).
29
+ u.searchParams.set("prompt", "consent");
30
+ return u.toString();
31
+ }
32
+ async function postToken(body, now) {
33
+ const res = await fetch(TOKEN_URL, {
34
+ method: "POST",
35
+ headers: { "content-type": "application/x-www-form-urlencoded" },
36
+ body: new URLSearchParams(body).toString(),
37
+ });
38
+ const json = (await res.json().catch(() => ({})));
39
+ if (!res.ok || !json.access_token) {
40
+ const detail = json.error_description || json.error || `${res.status}`;
41
+ throw new Error(`linear token endpoint failed: ${detail}`);
42
+ }
43
+ const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 86_399;
44
+ return {
45
+ accessToken: json.access_token,
46
+ ...(json.refresh_token ? { refreshToken: json.refresh_token } : {}),
47
+ expiresAt: new Date(now().getTime() + expiresIn * 1000),
48
+ ...(json.scope ? { scope: json.scope } : {}),
49
+ };
50
+ }
51
+ /** Exchange the callback `code` for access + refresh tokens (one-time, at install). */
52
+ export function exchangeCode(params, now = () => new Date()) {
53
+ return postToken({
54
+ grant_type: "authorization_code",
55
+ client_id: params.clientId,
56
+ client_secret: params.clientSecret,
57
+ code: params.code,
58
+ redirect_uri: params.redirectUri,
59
+ }, now);
60
+ }
61
+ /**
62
+ * Resolve the workspace (organization) id the access token belongs to. Used at install to backfill
63
+ * a connection's `workspaceIds` from the token, so an operator need not paste the org id by hand and
64
+ * intake can match the workspace on the very first webhook. Plain GraphQL fetch, no SDK.
65
+ */
66
+ export async function fetchOrganizationId(accessToken) {
67
+ const res = await fetch(GRAPHQL_URL, {
68
+ method: "POST",
69
+ headers: { "content-type": "application/json", authorization: accessToken },
70
+ body: JSON.stringify({ query: "query { organization { id } }" }),
71
+ });
72
+ const json = (await res.json().catch(() => ({})));
73
+ const id = json.data?.organization?.id;
74
+ if (!res.ok || !id) {
75
+ const detail = json.errors?.[0]?.message || `${res.status}`;
76
+ throw new Error(`linear organization query failed: ${detail}`);
77
+ }
78
+ return id;
79
+ }
80
+ /**
81
+ * Probe a stored access token: query `viewer` (the app/agent user) and `organization` (the bound
82
+ * workspace) so the portal can confirm a connection's token is valid and show which workspace it
83
+ * binds to. Throws on an auth failure / GraphQL error, which the caller renders as "token invalid".
84
+ */
85
+ export async function probeToken(accessToken) {
86
+ const res = await fetch(GRAPHQL_URL, {
87
+ method: "POST",
88
+ headers: { "content-type": "application/json", authorization: accessToken },
89
+ body: JSON.stringify({ query: "query { viewer { id name } organization { id name urlKey } }" }),
90
+ });
91
+ const json = (await res.json().catch(() => ({})));
92
+ if (!res.ok || json.errors?.length || !json.data) {
93
+ const detail = json.errors?.[0]?.message || `${res.status}`;
94
+ throw new Error(`linear probe failed: ${detail}`);
95
+ }
96
+ const v = json.data.viewer;
97
+ const o = json.data.organization;
98
+ return {
99
+ ...(v?.id ? { viewer: { id: v.id, ...(v.name ? { name: v.name } : {}) } } : {}),
100
+ ...(o?.id ? { organization: { id: o.id, ...(o.name ? { name: o.name } : {}), ...(o.urlKey ? { urlKey: o.urlKey } : {}) } } : {}),
101
+ };
102
+ }
103
+ /** Rotate the access token using the stored refresh token (the hub calls this near expiry / on 401). */
104
+ export function refreshTokens(params, now = () => new Date()) {
105
+ return postToken({
106
+ grant_type: "refresh_token",
107
+ client_id: params.clientId,
108
+ client_secret: params.clientSecret,
109
+ refresh_token: params.refreshToken,
110
+ }, now);
111
+ }
112
+ /**
113
+ * Mint an `app` actor token via the `client_credentials` grant: headless (no user, no redirect, no
114
+ * callback, no refresh token), scoped to the app's workspace, valid ~30 days. Linear's documented
115
+ * renewal is simply to mint a fresh one on a 401/expiry. Requires the app's "Client credentials"
116
+ * toggle to be enabled. This is the durable token path for a headless agent, vs the interactive
117
+ * authorization-code flow (24h token + fragile refresh + "already installed" install UX).
118
+ */
119
+ export function mintAppToken(params, now = () => new Date()) {
120
+ return postToken({
121
+ grant_type: "client_credentials",
122
+ client_id: params.clientId,
123
+ client_secret: params.clientSecret,
124
+ scope: (params.scopes ?? DEFAULT_AGENT_SCOPES).join(","),
125
+ }, now);
126
+ }
127
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,aAAa,GAAG,oCAAoC,CAAC;AAC3D,MAAM,SAAS,GAAG,oCAAoC,CAAC;AACvD,MAAM,WAAW,GAAG,gCAAgC,CAAC;AAErD,4FAA4F;AAC5F,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,CAAU,CAAC;AAWpG;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAM5B;IACC,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;IACjC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;IACjD,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;IACvD,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;IAC5C,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,MAAM,IAAI,oBAAoB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/E,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC;IACnD,8FAA8F;IAC9F,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACxC,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;AACtB,CAAC;AAWD,KAAK,UAAU,SAAS,CAAC,IAA4B,EAAE,GAAe;IACpE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;QACjC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;QAChE,IAAI,EAAE,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;KAC3C,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAkB,CAAC;IACnE,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClC,MAAM,MAAM,GAAG,IAAI,CAAC,iBAAiB,IAAI,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,iCAAiC,MAAM,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC;IACjF,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,YAAY;QAC9B,GAAG,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACnE,SAAS,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,SAAS,GAAG,IAAI,CAAC;QACvD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7C,CAAC;AACJ,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,YAAY,CAC1B,MAAqF,EACrF,MAAkB,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE;IAElC,OAAO,SAAS,CACd;QACE,UAAU,EAAE,oBAAoB;QAChC,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,aAAa,EAAE,MAAM,CAAC,YAAY;QAClC,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,YAAY,EAAE,MAAM,CAAC,WAAW;KACjC,EACD,GAAG,CACJ,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,WAAmB;IAC3D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,WAAW,EAAE;QACnC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,WAAW,EAAE;QAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,+BAA+B,EAAE,CAAC;KACjE,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAG/C,CAAC;IACF,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,CAAC;IACvC,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,qCAAqC,MAAM,EAAE,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAQD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,WAAmB;IAClD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,WAAW,EAAE;QACnC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,aAAa,EAAE,WAAW,EAAE;QAC3E,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,8DAA8D,EAAE,CAAC;KAChG,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAG/C,CAAC;IACF,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACjD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,wBAAwB,MAAM,EAAE,CAAC,CAAC;IACpD,CAAC;IACD,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;IAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC;IACjC,OAAO;QACL,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACjI,CAAC;AACJ,CAAC;AAED,wGAAwG;AACxG,MAAM,UAAU,aAAa,CAC3B,MAAwE,EACxE,MAAkB,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE;IAElC,OAAO,SAAS,CACd;QACE,UAAU,EAAE,eAAe;QAC3B,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,aAAa,EAAE,MAAM,CAAC,YAAY;QAClC,aAAa,EAAE,MAAM,CAAC,YAAY;KACnC,EACD,GAAG,CACJ,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,MAA8E,EAC9E,MAAkB,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE;IAElC,OAAO,SAAS,CACd;QACE,UAAU,EAAE,oBAAoB;QAChC,SAAS,EAAE,MAAM,CAAC,QAAQ;QAC1B,aAAa,EAAE,MAAM,CAAC,YAAY;QAClC,KAAK,EAAE,CAAC,MAAM,CAAC,MAAM,IAAI,oBAAoB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;KACzD,EACD,GAAG,CACJ,CAAC;AACJ,CAAC"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * A credential-free AgentSessionClient that records every call instead of touching
3
+ * Linear. It is the offline-acceptance counterpart to the mock Runner: the hub harness
4
+ * and unit tests inject it and assert on the ordered call log, so the whole control
5
+ * surface (plan, activities, elicitation, auth, issue-start, PR attach, state,
6
+ * externalUrls) is verified with no Linear credentials. The real `@linear/sdk` client is
7
+ * the live path (./linear-client.ts).
8
+ */
9
+ import type { Activity, AgentSessionClient, AuthRequest, DocumentInput, ElicitOption, ExternalUrl, PlanItem, PrAttachment, RepoCandidate, SessionState } from "./index.js";
10
+ export type RecordedCall = {
11
+ call: "postActivity";
12
+ sessionId: string;
13
+ activity: Activity;
14
+ } | {
15
+ call: "setPlan";
16
+ sessionId: string;
17
+ items: PlanItem[];
18
+ } | {
19
+ call: "raiseElicitation";
20
+ sessionId: string;
21
+ prompt: string;
22
+ options?: ElicitOption[];
23
+ } | {
24
+ call: "suggestRepositories";
25
+ issueId: string;
26
+ candidates: RepoCandidate[];
27
+ } | {
28
+ call: "addIssueLabel";
29
+ issueId: string;
30
+ name: string;
31
+ } | {
32
+ call: "requestAuth";
33
+ sessionId: string;
34
+ prompt: string;
35
+ auth: AuthRequest;
36
+ } | {
37
+ call: "startIssue";
38
+ sessionId: string;
39
+ } | {
40
+ call: "moveIssueToReview";
41
+ sessionId: string;
42
+ } | {
43
+ call: "commentOnIssue";
44
+ sessionId: string;
45
+ body: string;
46
+ } | {
47
+ call: "attachPr";
48
+ sessionId: string;
49
+ pr: PrAttachment;
50
+ } | {
51
+ call: "createDocument";
52
+ sessionId: string;
53
+ doc: DocumentInput;
54
+ } | {
55
+ call: "setExternalUrls";
56
+ sessionId: string;
57
+ urls: ExternalUrl[];
58
+ } | {
59
+ call: "setState";
60
+ sessionId: string;
61
+ state: SessionState;
62
+ };
63
+ export interface RecordingClient extends AgentSessionClient {
64
+ /** Every call made, in order. */
65
+ readonly calls: RecordedCall[];
66
+ }
67
+ export declare function createRecordingClient(): RecordingClient;
68
+ //# sourceMappingURL=recording-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recording-client.d.ts","sourceRoot":"","sources":["../src/recording-client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EACV,QAAQ,EACR,kBAAkB,EAClB,WAAW,EACX,aAAa,EACb,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,YAAY,EACb,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,GAC/D;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,QAAQ,EAAE,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,YAAY,EAAE,CAAA;CAAE,GACzF;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,aAAa,EAAE,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACxD;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC7E;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,YAAY,CAAA;CAAE,GACzD;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,aAAa,CAAA;CAAE,GACjE;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,EAAE,CAAA;CAAE,GACnE;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,YAAY,CAAA;CAAE,CAAC;AAEjE,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD,iCAAiC;IACjC,QAAQ,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC;CAChC;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAkDvD"}
@@ -0,0 +1,52 @@
1
+ export function createRecordingClient() {
2
+ const calls = [];
3
+ return {
4
+ calls,
5
+ async postActivity(sessionId, activity) {
6
+ calls.push({ call: "postActivity", sessionId, activity });
7
+ },
8
+ async setPlan(sessionId, items) {
9
+ calls.push({ call: "setPlan", sessionId, items });
10
+ },
11
+ async raiseElicitation(sessionId, prompt, options) {
12
+ calls.push({ call: "raiseElicitation", sessionId, prompt, ...(options ? { options } : {}) });
13
+ },
14
+ async suggestRepositories(issueId, candidates) {
15
+ calls.push({ call: "suggestRepositories", issueId, candidates });
16
+ // No opinion offline: the caller then falls back to registry order (exercised by the unit tests
17
+ // of the pure ordering function, which take real suggestions).
18
+ return [];
19
+ },
20
+ async addIssueLabel(issueId, name) {
21
+ calls.push({ call: "addIssueLabel", issueId, name });
22
+ },
23
+ async requestAuth(sessionId, prompt, auth) {
24
+ calls.push({ call: "requestAuth", sessionId, prompt, auth });
25
+ },
26
+ async startIssue(sessionId) {
27
+ calls.push({ call: "startIssue", sessionId });
28
+ },
29
+ async moveIssueToReview(sessionId) {
30
+ calls.push({ call: "moveIssueToReview", sessionId });
31
+ },
32
+ async commentOnIssue(sessionId, body) {
33
+ calls.push({ call: "commentOnIssue", sessionId, body });
34
+ },
35
+ async attachPr(sessionId, pr) {
36
+ calls.push({ call: "attachPr", sessionId, pr });
37
+ },
38
+ async createDocument(sessionId, doc) {
39
+ calls.push({ call: "createDocument", sessionId, doc });
40
+ // Deterministic fake id/url so offline runs and tests have a stable document reference.
41
+ const slug = doc.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
42
+ return { id: `doc-${slug}`, url: `https://linear.app/doc/${slug}` };
43
+ },
44
+ async setExternalUrls(sessionId, urls) {
45
+ calls.push({ call: "setExternalUrls", sessionId, urls });
46
+ },
47
+ async setState(sessionId, state) {
48
+ calls.push({ call: "setState", sessionId, state });
49
+ },
50
+ };
51
+ }
52
+ //# sourceMappingURL=recording-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recording-client.js","sourceRoot":"","sources":["../src/recording-client.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@dahrk/linear",
3
+ "version": "0.1.0",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "description": "The Linear Agent Session API client and webhook verification. The native control surface: activities, agent-plan checklist, elicitation gates, prompted-drive, stop signal, externalUrls.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/skakel/dahrk.git"
10
+ },
11
+ "homepage": "https://github.com/skakel/dahrk#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/skakel/dahrk/issues"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@linear/sdk": "^86.0.0",
29
+ "@dahrk/contracts": "0.1.0"
30
+ },
31
+ "devDependencies": {
32
+ "tsx": "^4.19.0"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit",
37
+ "test": "node --import tsx --test test/*.test.ts",
38
+ "spike/live": "tsx spike/run.ts"
39
+ },
40
+ "main": "./dist/index.js",
41
+ "types": "./dist/index.d.ts"
42
+ }