pi-provider-qoder 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +19 -5
  2. package/dist/index.js +178 -211
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -30,11 +30,25 @@ Then log in from pi:
30
30
  /login qoder
31
31
  ```
32
32
 
33
- The login flow will prompt you to complete authorization in your browser.
33
+ The login menu offers two methods:
34
34
 
35
- ### Personal Access Token (PAT) Fallback
35
+ - **Browser Login** OAuth device-code flow; complete authorization in your browser.
36
+ - **Use API Key (PAT)** — paste a Qoder Personal Access Token (`pt-...`).
36
37
 
37
- For non-interactive environments, you can set the `QODER_PERSONAL_ACCESS_TOKEN` (or `QODER_PAT`) environment variable before starting pi. The provider will automatically pick up the PAT and authenticate.
38
+ ### Personal Access Token (PAT)
39
+
40
+ A Qoder PAT (`pt-...`) cannot authenticate API calls directly — the provider
41
+ exchanges it for a short-lived job token (mirroring the official `qodercli`
42
+ flow) and resolves your account identity automatically. You can supply a PAT in
43
+ two ways:
44
+
45
+ - Run `/login qoder` and choose **Use API Key (PAT)**, then paste the token.
46
+ - Set the `QODER_PERSONAL_ACCESS_TOKEN` (or `QODER_PAT`) environment variable,
47
+ then run `/login qoder` — the PAT is picked up automatically and exchanged
48
+ without further prompts. This is the recommended path for headless/CI setups.
49
+
50
+ > The exchanged job token is short-lived; the provider transparently re-exchanges
51
+ > the stored PAT when it expires.
38
52
 
39
53
  ## Models
40
54
 
@@ -70,8 +84,8 @@ Or let Qoder select automatically:
70
84
  src/
71
85
  ├── index.ts # Extension registration
72
86
  ├── cosy.ts # COSY Signature and Machine ID resolver
73
- ├── login.ts # OAuth Device Flow login sequence
74
- ├── login-ui.ts # Custom TUI components for login
87
+ ├── login.ts # OAuth Device Flow + PAT login sequence
88
+ ├── pat.ts # PAT job-token exchange + identity resolution
75
89
  ├── models.ts # Model definitions and Dynamic Config Cache
76
90
  ├── oauth.ts # PAT / OAuth callback orchestrator
77
91
  ├── stream.ts # Main streaming response handler
package/dist/index.js CHANGED
@@ -1,128 +1,3 @@
1
- // src/login-ui.ts
2
- import { DynamicBorder } from "@earendil-works/pi-coding-agent";
3
- import { Container, SelectList, Text } from "@earendil-works/pi-tui";
4
- var _ctx;
5
- function setExtensionContext(ctx) {
6
- _ctx = ctx;
7
- }
8
- function hasExtensionContext() {
9
- return _ctx !== void 0;
10
- }
11
- async function showLoginUI() {
12
- if (!_ctx) return null;
13
- const ctx = _ctx;
14
- return ctx.ui.custom((tui, theme, _kb, done) => {
15
- const mainItems = [
16
- { value: "web", label: "Browser Login", description: "Sign in via browser (OAuth device flow)" }
17
- ];
18
- const container = new Container();
19
- const border = new DynamicBorder((s) => theme.fg("accent", s));
20
- const title = new Text(theme.fg("accent", theme.bold("Qoder Login")), 1, 0);
21
- const hint = new Text(theme.fg("dim", "\u2191\u2193 navigate \u2022 enter select \u2022 esc cancel"), 1, 0);
22
- const borderBottom = new DynamicBorder((s) => theme.fg("accent", s));
23
- const selectList = new SelectList(mainItems, mainItems.length, {
24
- selectedPrefix: (t) => theme.fg("accent", t),
25
- selectedText: (t) => theme.fg("accent", t),
26
- description: (t) => theme.fg("muted", t),
27
- scrollInfo: (t) => theme.fg("dim", t),
28
- noMatch: (t) => theme.fg("warning", t)
29
- });
30
- selectList.onSelect = (item) => {
31
- done({ method: item.value });
32
- };
33
- selectList.onCancel = () => done(null);
34
- container.addChild(border);
35
- container.addChild(title);
36
- container.addChild(selectList);
37
- container.addChild(hint);
38
- container.addChild(borderBottom);
39
- tui.requestRender();
40
- return {
41
- render(width) {
42
- return container.render(width);
43
- },
44
- invalidate() {
45
- container.invalidate();
46
- },
47
- handleInput(data) {
48
- selectList.handleInput(data);
49
- tui.requestRender();
50
- }
51
- };
52
- });
53
- }
54
- async function showWaitingUI(outerCallbacks, runAuth) {
55
- if (!_ctx) {
56
- return runAuth(outerCallbacks);
57
- }
58
- const ctx = _ctx;
59
- return ctx.ui.custom((tui, theme, _kb, done) => {
60
- const container = new Container();
61
- const border = new DynamicBorder((s) => theme.fg("accent", s));
62
- const title = new Text(theme.fg("accent", theme.bold("Qoder Login - Authorization")), 1, 0);
63
- const borderBottom = new DynamicBorder((s) => theme.fg("accent", s));
64
- const statusText = new Text("Initiating login flow...", 1, 0);
65
- const urlText = new Text("", 1, 0);
66
- const instructionsText = new Text("", 1, 0);
67
- const hint = new Text(theme.fg("dim", "esc cancel / back"), 1, 0);
68
- container.addChild(border);
69
- container.addChild(title);
70
- container.addChild(statusText);
71
- container.addChild(urlText);
72
- container.addChild(instructionsText);
73
- container.addChild(hint);
74
- container.addChild(borderBottom);
75
- const abortCtrl = new AbortController();
76
- let onAuthCalled = false;
77
- const mergedCallbacks = {
78
- ...outerCallbacks,
79
- onProgress: (msg) => {
80
- outerCallbacks.onProgress?.(msg);
81
- statusText.setText(msg);
82
- tui.requestRender();
83
- },
84
- onAuth: (info) => {
85
- if (!onAuthCalled) {
86
- onAuthCalled = true;
87
- outerCallbacks.onAuth?.(info);
88
- }
89
- urlText.setText(`URL: ${info.url}`);
90
- instructionsText.setText(info.instructions || "");
91
- tui.requestRender();
92
- },
93
- signal: abortCtrl.signal
94
- };
95
- runAuth(mergedCallbacks).then(
96
- (creds) => {
97
- done(creds);
98
- },
99
- (err) => {
100
- if (abortCtrl.signal.aborted) {
101
- done(null);
102
- } else {
103
- statusText.setText(theme.fg("warning", `Error: ${err.message || err}`));
104
- tui.requestRender();
105
- setTimeout(() => done(null), 3e3);
106
- }
107
- }
108
- );
109
- return {
110
- render(width) {
111
- return container.render(width);
112
- },
113
- invalidate() {
114
- container.invalidate();
115
- },
116
- handleInput(data) {
117
- if (data.length === 1 && data.charCodeAt(0) === 27 || data === "q") {
118
- abortCtrl.abort();
119
- done(null);
120
- }
121
- }
122
- };
123
- });
124
- }
125
-
126
1
  // src/models.ts
127
2
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
128
3
  import { homedir as homedir2 } from "node:os";
@@ -425,7 +300,7 @@ function getCachedModelConfig(modelKey) {
425
300
  if (existsSync2(CACHE_PATH)) {
426
301
  try {
427
302
  const data = JSON.parse(readFileSync2(CACHE_PATH, "utf8"));
428
- if (data && data.configs && data.configs[modelKey]) {
303
+ if (data?.configs?.[modelKey]) {
429
304
  return data.configs[modelKey];
430
305
  }
431
306
  } catch {
@@ -534,6 +409,101 @@ import { join as join3 } from "node:path";
534
409
 
535
410
  // src/login.ts
536
411
  import crypto2 from "node:crypto";
412
+
413
+ // src/pat.ts
414
+ var UA = "pi-provider-qoder";
415
+ var EXCHANGE_URL = "https://openapi.qoder.sh/api/v1/jobToken/exchange";
416
+ var USERINFO_URL = "https://openapi.qoder.sh/api/v1/userinfo";
417
+ var PAT_REFRESH_PREFIX = "pat";
418
+ function isPatRefresh(refresh) {
419
+ return refresh.startsWith(`${PAT_REFRESH_PREFIX}|`);
420
+ }
421
+ function encodePatRefresh(pat, jobRefreshToken, userID, machineID) {
422
+ return [PAT_REFRESH_PREFIX, pat, jobRefreshToken, userID, machineID].join("|");
423
+ }
424
+ function decodePatRefresh(refresh) {
425
+ const parts = refresh.split("|");
426
+ return {
427
+ pat: parts[1] || "",
428
+ jobRefreshToken: parts[2] || "",
429
+ userID: parts[3] || "",
430
+ machineID: parts[4] || ""
431
+ };
432
+ }
433
+ async function exchangeJobToken(pat) {
434
+ const res = await fetch(EXCHANGE_URL, {
435
+ method: "POST",
436
+ headers: {
437
+ "Content-Type": "application/json",
438
+ Accept: "application/json",
439
+ "User-Agent": UA,
440
+ "Cosy-Version": "1.0.1",
441
+ "Cosy-ClientType": "5"
442
+ },
443
+ body: JSON.stringify({ personal_token: pat })
444
+ });
445
+ if (!res.ok) {
446
+ const text = await res.text().catch(() => "");
447
+ throw new Error(`Qoder PAT exchange failed: ${res.status} ${res.statusText}. ${text.slice(0, 200)}`);
448
+ }
449
+ const data = await res.json();
450
+ if (!data.token) {
451
+ throw new Error("Qoder PAT exchange returned no job token");
452
+ }
453
+ let expiresAt = Date.now() + 24 * 60 * 60 * 1e3;
454
+ if (data.expires_at) {
455
+ const parsed = Date.parse(data.expires_at);
456
+ if (!Number.isNaN(parsed)) expiresAt = parsed;
457
+ } else if (data.expires_in) {
458
+ expiresAt = Date.now() + data.expires_in;
459
+ }
460
+ return {
461
+ jobToken: data.token,
462
+ jobRefreshToken: data.refresh_token || "",
463
+ expiresAt
464
+ };
465
+ }
466
+ async function fetchUserInfo(jobToken) {
467
+ let userID = "";
468
+ let email = "";
469
+ let name = "";
470
+ try {
471
+ const res = await fetch(USERINFO_URL, {
472
+ headers: {
473
+ Authorization: `Bearer ${jobToken}`,
474
+ Accept: "application/json",
475
+ "User-Agent": UA,
476
+ "Cosy-Version": "1.0.1",
477
+ "Cosy-ClientType": "5"
478
+ }
479
+ });
480
+ if (res.ok) {
481
+ const info = await res.json();
482
+ userID = info.id || "";
483
+ email = info.email || "";
484
+ name = info.name || info.username || "";
485
+ }
486
+ } catch {
487
+ }
488
+ return { userID, email, name };
489
+ }
490
+ async function credentialsFromPat(pat) {
491
+ const { jobToken, jobRefreshToken, expiresAt } = await exchangeJobToken(pat);
492
+ const { userID, email, name } = await fetchUserInfo(jobToken);
493
+ const machineID = getMachineId();
494
+ return {
495
+ refresh: encodePatRefresh(pat, jobRefreshToken, userID, machineID),
496
+ access: jobToken,
497
+ expires: expiresAt - 5 * 60 * 1e3,
498
+ // 5 min buffer
499
+ userID,
500
+ email,
501
+ name,
502
+ machineID
503
+ };
504
+ }
505
+
506
+ // src/login.ts
537
507
  function getPrompt(callbacks) {
538
508
  return callbacks.onPrompt;
539
509
  }
@@ -561,29 +531,39 @@ function parseExpiresAt(s, expiresInSeconds) {
561
531
  return Date.now() + 30 * 24 * 60 * 60 * 1e3;
562
532
  }
563
533
  async function interactiveLogin(callbacks) {
564
- if (hasExtensionContext()) {
565
- const choice = await showLoginUI();
566
- if (!choice) {
567
- throw new Error("Login cancelled");
568
- }
569
- const runAuth = async (mergedCallbacks) => {
570
- return runDeviceFlow(mergedCallbacks);
571
- };
572
- const creds = await showWaitingUI(callbacks, runAuth);
573
- if (!creds) {
574
- throw new Error("Login cancelled");
575
- }
576
- return creds;
577
- }
578
534
  const prompt = getPrompt(callbacks);
579
- const proceed = await prompt({
580
- message: "Press Enter to start browser login for Qoder",
581
- placeholder: "press enter",
535
+ const pat = await prompt({
536
+ message: "Paste a Qoder Personal Access Token (pt-...), or leave empty for browser login",
537
+ placeholder: "pt-...",
582
538
  allowEmpty: true
583
539
  });
584
540
  if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
541
+ if (pat?.trim()) {
542
+ return patLogin(callbacks, pat.trim());
543
+ }
544
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
585
545
  return runDeviceFlow(callbacks);
586
546
  }
547
+ async function patLogin(callbacks, providedPat) {
548
+ let pat = providedPat;
549
+ if (!pat) {
550
+ const prompt = getPrompt(callbacks);
551
+ const entered = await prompt({
552
+ message: "Paste your Qoder Personal Access Token (pt-...)",
553
+ placeholder: "pt-...",
554
+ allowEmpty: false
555
+ });
556
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
557
+ pat = entered?.trim();
558
+ }
559
+ if (!pat) {
560
+ throw new Error("No Personal Access Token provided");
561
+ }
562
+ getProgress(callbacks)?.("Exchanging access token...");
563
+ const creds = await credentialsFromPat(pat);
564
+ getProgress(callbacks)?.("Login successful!");
565
+ return creds;
566
+ }
587
567
  function abortableDelay(ms, signal) {
588
568
  if (signal?.aborted) return Promise.reject(signal.reason || new Error("Login cancelled"));
589
569
  return new Promise((resolve, reject) => {
@@ -666,7 +646,8 @@ async function runDeviceFlow(callbacks) {
666
646
  machineID
667
647
  };
668
648
  } catch (e) {
669
- if (e.name === "AbortError" || getSignal(callbacks)?.aborted) {
649
+ const err = e;
650
+ if (err.name === "AbortError" || getSignal(callbacks)?.aborted) {
670
651
  throw new Error("Login cancelled");
671
652
  }
672
653
  throw e;
@@ -682,7 +663,7 @@ function getCachedCredentials(_accessToken) {
682
663
  try {
683
664
  const auth = JSON.parse(readFileSync3(AUTH_FILE, "utf-8"));
684
665
  const creds = auth?.qoder;
685
- if (creds && creds.userID) {
666
+ if (creds?.userID) {
686
667
  return creds;
687
668
  }
688
669
  } catch {
@@ -694,33 +675,11 @@ async function loginQoder(callbacks) {
694
675
  const pat = process.env.QODER_PERSONAL_ACCESS_TOKEN || process.env.QODER_PAT;
695
676
  if (pat) {
696
677
  try {
697
- const userinfoRes = await fetch("https://openapi.qoder.sh/api/v1/userinfo", {
698
- headers: {
699
- Authorization: `Bearer ${pat}`,
700
- Accept: "application/json",
701
- "User-Agent": "pi-provider-qoder"
702
- }
678
+ const creds2 = await credentialsFromPat(pat);
679
+ const qCreds = creds2;
680
+ updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email).catch(() => {
703
681
  });
704
- if (userinfoRes.ok) {
705
- const userinfo = await userinfoRes.json();
706
- const email = userinfo.email || "";
707
- const name = userinfo.name || userinfo.username || "";
708
- const userID = userinfo.id || "pat";
709
- const machineID = getMachineId();
710
- const creds2 = {
711
- refresh: `pat|${userID}|${machineID}`,
712
- access: pat,
713
- expires: Date.now() + 30 * 24 * 60 * 60 * 1e3,
714
- // 30 days
715
- userID,
716
- email,
717
- name,
718
- machineID
719
- };
720
- updateQoderModelsCache(pat, userID, name, email).catch(() => {
721
- });
722
- return creds2;
723
- }
682
+ return creds2;
724
683
  } catch {
725
684
  }
726
685
  }
@@ -734,16 +693,31 @@ async function loginQoder(callbacks) {
734
693
  return creds;
735
694
  }
736
695
  async function refreshQoderToken(credentials) {
737
- const parts = credentials.refresh.split("|");
738
- const refreshToken = parts[0] || "";
739
- const userID = parts[1] || "";
740
- const machineID = parts[2] || getMachineId();
741
- if (refreshToken === "pat") {
696
+ if (isPatRefresh(credentials.refresh)) {
697
+ const { pat } = decodePatRefresh(credentials.refresh);
698
+ if (pat) {
699
+ try {
700
+ const refreshed = await credentialsFromPat(pat);
701
+ const qCreds = refreshed;
702
+ updateQoderModelsCache(qCreds.access, qCreds.userID, qCreds.name, qCreds.email).catch(() => {
703
+ });
704
+ return refreshed;
705
+ } catch {
706
+ }
707
+ }
742
708
  return {
743
709
  ...credentials,
744
- expires: Date.now() + 30 * 24 * 60 * 60 * 1e3
710
+ expires: Date.now() + 60 * 60 * 1e3
711
+ // extend 1 hour to retry later
745
712
  };
746
713
  }
714
+ const parts = credentials.refresh.split("|");
715
+ const refreshToken = parts[0] || "";
716
+ const userID = parts[1] || "";
717
+ const machineID = parts[2] || getMachineId();
718
+ const prev = credentials;
719
+ const prevName = prev.name || "";
720
+ const prevEmail = prev.email || "";
747
721
  const refreshURL = "https://center.qoder.sh/algo/api/v3/user/refresh_token";
748
722
  try {
749
723
  const response = await fetch(refreshURL, {
@@ -773,16 +747,11 @@ async function refreshQoderToken(credentials) {
773
747
  access: newAccess,
774
748
  expires: expireMs - 5 * 60 * 1e3,
775
749
  userID,
776
- email: credentials.email || "",
777
- name: credentials.name || "",
750
+ email: prevEmail,
751
+ name: prevName,
778
752
  machineID
779
753
  };
780
- updateQoderModelsCache(
781
- newAccess,
782
- userID,
783
- credentials.name || "",
784
- credentials.email || ""
785
- ).catch(() => {
754
+ updateQoderModelsCache(newAccess, userID, prevName, prevEmail).catch(() => {
786
755
  });
787
756
  return refreshed;
788
757
  }
@@ -1046,7 +1015,7 @@ function transformMessagesForQoder(messages) {
1046
1015
  };
1047
1016
  }
1048
1017
  return null;
1049
- }).filter(Boolean);
1018
+ }).filter((p) => p !== null);
1050
1019
  } else {
1051
1020
  content = getContentText(msg);
1052
1021
  }
@@ -1118,11 +1087,11 @@ function stableChatRecordID(model, messages, tools, maxTokens) {
1118
1087
  hash.update("\0");
1119
1088
  hash.update(model);
1120
1089
  for (const msg of messages) {
1121
- if (msg && msg.role) {
1090
+ if (msg?.role) {
1122
1091
  hash.update("\0");
1123
1092
  hash.update(msg.role);
1124
1093
  }
1125
- if (msg && msg.content) {
1094
+ if (msg?.content) {
1126
1095
  hash.update("\0");
1127
1096
  hash.update(typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content));
1128
1097
  }
@@ -1186,7 +1155,7 @@ function streamQoder(model, context, options) {
1186
1155
  for (let i = normalizedMessages.length - 1; i >= 0; i--) {
1187
1156
  if (normalizedMessages[i].role === "user") {
1188
1157
  const content = normalizedMessages[i].content;
1189
- lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => c.text || "").join("") : "";
1158
+ lastUserText = typeof content === "string" ? content : Array.isArray(content) ? content.map((c) => "text" in c ? c.text : "").join("") : "";
1190
1159
  break;
1191
1160
  }
1192
1161
  }
@@ -1361,7 +1330,7 @@ function streamQoder(model, context, options) {
1361
1330
  for (const tc of delta.tool_calls) {
1362
1331
  const idx = tc.index ?? 0;
1363
1332
  if (!toolCallsState[idx]) {
1364
- toolCallsState[idx] = { arguments: "" };
1333
+ toolCallsState[idx] = { arguments: "", id: "", name: "", contentIndex: 0 };
1365
1334
  }
1366
1335
  const state = toolCallsState[idx];
1367
1336
  if (tc.id) state.id = tc.id;
@@ -1407,7 +1376,7 @@ function streamQoder(model, context, options) {
1407
1376
  });
1408
1377
  }
1409
1378
  for (const state of toolCallsState) {
1410
- if (state && state.emittedStart && !state.emittedEnd) {
1379
+ if (state?.emittedStart && !state.emittedEnd) {
1411
1380
  state.emittedEnd = true;
1412
1381
  let args = {};
1413
1382
  try {
@@ -1498,30 +1467,28 @@ async function fetchQoderUsage(credentials) {
1498
1467
 
1499
1468
  // src/index.ts
1500
1469
  function index_default(pi) {
1501
- pi.on("session_start", async (_event, ctx) => {
1502
- setExtensionContext(ctx);
1503
- });
1470
+ const oauth = {
1471
+ name: "Qoder (Browser OAuth / PAT)",
1472
+ login: loginQoder,
1473
+ refreshToken: refreshQoderToken,
1474
+ getApiKey: (cred) => cred.access,
1475
+ modifyModels: (models, _cred) => {
1476
+ const cached = getCachedModels();
1477
+ const nonQoder = models.filter((m) => m.provider !== "qoder");
1478
+ const modelsToUse = cached.length > 0 ? cached : staticModels;
1479
+ const modifiedQoder = modelsToUse.map((m) => ({
1480
+ ...m,
1481
+ baseUrl: "https://api3.qoder.sh/"
1482
+ }));
1483
+ return [...nonQoder, ...modifiedQoder];
1484
+ },
1485
+ fetchUsage: fetchQoderUsage
1486
+ };
1504
1487
  pi.registerProvider("qoder", {
1505
1488
  baseUrl: "https://api3.qoder.sh/",
1506
1489
  api: "qoder-api",
1507
1490
  models: getCachedModels(),
1508
- oauth: {
1509
- name: "Qoder (Browser OAuth / PAT)",
1510
- login: loginQoder,
1511
- refreshToken: refreshQoderToken,
1512
- getApiKey: (cred) => cred.access,
1513
- modifyModels: (models, cred) => {
1514
- const cached = getCachedModels();
1515
- const nonQoder = models.filter((m) => m.provider !== "qoder");
1516
- const modelsToUse = cached.length > 0 ? cached : staticModels;
1517
- const modifiedQoder = modelsToUse.map((m) => ({
1518
- ...m,
1519
- baseUrl: "https://api3.qoder.sh/"
1520
- }));
1521
- return [...nonQoder, ...modifiedQoder];
1522
- },
1523
- fetchUsage: fetchQoderUsage
1524
- },
1491
+ oauth,
1525
1492
  streamSimple: streamQoder
1526
1493
  });
1527
1494
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-qoder",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Pi extension for Qoder AI — with OAuth authentication, COSY signatures and WAF bypass",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@
19
19
  "README.md"
20
20
  ],
21
21
  "scripts": {
22
- "build": "./node_modules/esbuild/bin/esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@earendil-works/pi-ai --external:@earendil-works/pi-coding-agent --external:@earendil-works/pi-tui",
22
+ "build": "./node_modules/esbuild/bin/esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@earendil-works/pi-ai --external:@earendil-works/pi-coding-agent",
23
23
  "check": "node node_modules/typescript/bin/tsc --noEmit",
24
24
  "lint": "node node_modules/@biomejs/biome/bin/biome check .",
25
25
  "lint:fix": "node node_modules/@biomejs/biome/bin/biome check --write .",
@@ -36,7 +36,6 @@
36
36
  "@biomejs/biome": "2.4.2",
37
37
  "@earendil-works/pi-ai": "^0.75.5",
38
38
  "@earendil-works/pi-coding-agent": "^0.75.5",
39
- "@earendil-works/pi-tui": "^0.75.5",
40
39
  "esbuild": "^0.25.0",
41
40
  "typescript": "^5.7.0"
42
41
  },