opencode-kiro 0.1.1 → 0.1.3

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.
@@ -33,10 +33,11 @@ function sumSessionCredits(messages, partsByMessage) {
33
33
  if (!hit) return acc;
34
34
  return {
35
35
  total: acc.total + hit.credits,
36
- unit: hit.unit ?? acc.unit
36
+ unit: hit.unit ?? acc.unit,
37
+ present: true
37
38
  };
38
39
  },
39
- { total: 0, unit: void 0 }
40
+ { total: 0, unit: void 0, present: false }
40
41
  );
41
42
  }
42
43
  var creditsAmount = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 });
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  formatCredits,
3
3
  sumSessionCredits
4
- } from "./chunk-JO7U4OSF.js";
4
+ } from "./chunk-JHCHA2UV.js";
5
5
 
6
6
  // src/tui/context-view.ts
7
7
  import { createElement, effect, insert, insertNode, setProp } from "@opentui/solid";
8
8
  import { createMemo } from "solid-js";
9
+ var money = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
9
10
  function createContextView(api, sessionID) {
10
11
  const theme = () => api.theme.current;
11
12
  const messages = createMemo(() => api.state.session.messages(sessionID));
@@ -27,6 +28,7 @@ function createContextView(api, sessionID) {
27
28
  };
28
29
  });
29
30
  const credits = createMemo(() => sumSessionCredits(messages(), (messageID) => api.state.part(messageID)));
31
+ const cost = createMemo(() => api.state.session.get(sessionID)?.cost ?? 0);
30
32
  const root = createElement("box");
31
33
  insertNode(root, headerLine(theme));
32
34
  insertNode(
@@ -39,7 +41,10 @@ function createContextView(api, sessionID) {
39
41
  );
40
42
  insertNode(
41
43
  root,
42
- mutedLine(theme, () => formatCredits(credits().total, credits().unit))
44
+ mutedLine(
45
+ theme,
46
+ () => credits().present ? formatCredits(credits().total, credits().unit) : `${money.format(cost())} spent`
47
+ )
43
48
  );
44
49
  return root;
45
50
  }
@@ -0,0 +1,20 @@
1
+ import {
2
+ formatCredits,
3
+ sumSessionCredits
4
+ } from "./chunk-JHCHA2UV.js";
5
+
6
+ // src/tui/credits-chip-view.ts
7
+ import { createElement, effect, insert, setProp } from "@opentui/solid";
8
+ import { createMemo } from "solid-js";
9
+ function createCreditsChipView(api, sessionID) {
10
+ const messages = createMemo(() => api.state.session.messages(sessionID));
11
+ const credits = createMemo(() => sumSessionCredits(messages(), (messageID) => api.state.part(messageID)));
12
+ const chip = createElement("text");
13
+ effect(() => setProp(chip, "fg", api.theme.current.textMuted));
14
+ setProp(chip, "wrapMode", "none");
15
+ insert(chip, () => credits().present ? formatCredits(credits().total, credits().unit) : "");
16
+ return chip;
17
+ }
18
+ export {
19
+ createCreditsChipView
20
+ };
package/dist/server.d.ts CHANGED
@@ -1,9 +1,19 @@
1
1
  import { Plugin, PluginInput, Hooks } from '@opencode-ai/plugin';
2
2
 
3
+ declare function kiroTokenPath(): string;
4
+ declare function readToken(tokenPath: string | undefined): Promise<{
5
+ type: "success";
6
+ refresh: string;
7
+ access: string;
8
+ expires: number;
9
+ } | {
10
+ type: "failed";
11
+ }>;
12
+ declare function notifyIfTokenExpired(client: PluginInput["client"] | undefined): Promise<void>;
3
13
  declare const KiroAuthPlugin: Plugin;
4
14
  declare const _default: {
5
15
  id: string;
6
16
  server: (input: PluginInput) => Promise<Hooks>;
7
17
  };
8
18
 
9
- export { KiroAuthPlugin, _default as default };
19
+ export { KiroAuthPlugin, _default as default, kiroTokenPath, notifyIfTokenExpired, readToken };
package/dist/server.js CHANGED
@@ -1,93 +1,208 @@
1
1
  // src/server.ts
2
- var server = async (input) => ({
3
- auth: {
4
- provider: "kiro",
5
- // Returned options are forwarded into createKiroAcp({...}). Relays each
6
- // catalog model's limit.context into contextWindows keyed by api.id;
7
- // zero/missing limits are skipped (SDK falls back to 1M).
8
- loader: async (_getAuth, provider) => ({
9
- cwd: input.directory ?? input.worktree,
10
- agent: "opencode",
11
- trustAllTools: true,
12
- mcpTimeout: 45,
13
- contextWindows: Object.fromEntries(
14
- Object.values(provider.models).filter((m) => (m.limit?.context ?? 0) > 0).map((m) => [m.api.id, m.limit.context])
15
- )
16
- }),
17
- methods: [
18
- {
19
- type: "oauth",
20
- label: "Kiro CLI Login",
21
- async authorize() {
22
- const { verifyAuth } = await import("kiro-acp-ai-provider");
23
- const status = verifyAuth();
24
- if (!status.installed)
25
- throw new Error(
26
- "kiro-cli is not installed. Install it from https://kiro.dev/docs/cli/"
27
- );
28
- if (status.authenticated) {
2
+ import { homedir } from "os";
3
+ import { dirname, join } from "path";
4
+ var KIRO_PLUGIN_NAME = "opencode-kiro";
5
+ var SIDEBAR_PLUGIN_ID = "internal:sidebar-context";
6
+ var server = async (input) => {
7
+ await notifyIfTokenExpired(input.client);
8
+ const tuiPath = tuiConfigPath();
9
+ const alreadyConfigured = isSidebarConfigured(await readTuiConfig(tuiPath));
10
+ const prompts = alreadyConfigured ? [] : [
11
+ {
12
+ type: "select",
13
+ key: "sidebar",
14
+ message: "Enable the Kiro credits sidebar?",
15
+ options: [
16
+ { label: "Yes", value: "yes", hint: "writes tui.json; restart to apply" },
17
+ { label: "No", value: "no" }
18
+ ]
19
+ }
20
+ ];
21
+ return {
22
+ auth: {
23
+ provider: "kiro",
24
+ // Returned options are forwarded into createKiroAcp({...}). Relays each
25
+ // catalog model's limit.context into contextWindows keyed by api.id;
26
+ // zero/missing limits are skipped (SDK falls back to 1M).
27
+ loader: async (_getAuth, provider) => ({
28
+ cwd: input.directory ?? input.worktree,
29
+ agent: "opencode",
30
+ trustAllTools: true,
31
+ mcpTimeout: 45,
32
+ contextWindows: Object.fromEntries(
33
+ Object.values(provider.models).filter((m) => (m.limit?.context ?? 0) > 0).map((m) => [m.api.id, m.limit.context])
34
+ )
35
+ }),
36
+ methods: [
37
+ {
38
+ type: "oauth",
39
+ label: "Kiro CLI Login",
40
+ prompts,
41
+ async authorize(inputs) {
42
+ const { verifyAuth } = await import("kiro-acp-ai-provider");
43
+ const status = verifyAuth();
44
+ if (!status.installed)
45
+ throw new Error(
46
+ "kiro-cli is not installed. Install it from https://kiro.dev/docs/cli/"
47
+ );
48
+ const enableSidebar = !alreadyConfigured && inputs?.sidebar === "yes";
49
+ const onSuccess = async () => {
50
+ if (enableSidebar) await enableSidebarConfig(tuiPath, input);
51
+ };
52
+ if (status.authenticated) {
53
+ return {
54
+ url: "",
55
+ instructions: "",
56
+ method: "auto",
57
+ async callback() {
58
+ const result = await readToken(status.tokenPath);
59
+ if (result.type === "success") await onSuccess();
60
+ return result;
61
+ }
62
+ };
63
+ }
64
+ const { execFile } = await import("child_process");
65
+ const child = execFile("kiro-cli", ["login"], {
66
+ shell: process.platform === "win32"
67
+ });
29
68
  return {
30
69
  url: "",
31
- instructions: "",
70
+ instructions: "Complete Kiro authentication in the browser window that just opened. Waiting for login...",
32
71
  method: "auto",
33
72
  async callback() {
34
- return readToken(status.tokenPath);
73
+ const maxWait = 12e4;
74
+ const start = Date.now();
75
+ while (Date.now() - start < maxWait) {
76
+ await new Promise((r) => setTimeout(r, 2e3));
77
+ const check = verifyAuth();
78
+ if (check.authenticated) {
79
+ child.kill();
80
+ const result = await readToken(check.tokenPath);
81
+ if (result.type === "success") await onSuccess();
82
+ return result;
83
+ }
84
+ }
85
+ child.kill();
86
+ throw new Error(
87
+ "Kiro authentication timed out. Run `kiro-cli auth login` manually."
88
+ );
35
89
  }
36
90
  };
37
91
  }
38
- const { execFile } = await import("child_process");
39
- const child = execFile("kiro-cli", ["login"]);
40
- return {
41
- url: "",
42
- instructions: "Complete Kiro authentication in the browser window that just opened. Waiting for login...",
43
- method: "auto",
44
- async callback() {
45
- const maxWait = 12e4;
46
- const start = Date.now();
47
- while (Date.now() - start < maxWait) {
48
- await new Promise((r) => setTimeout(r, 2e3));
49
- const check = verifyAuth();
50
- if (check.authenticated) {
51
- child.kill();
52
- return readToken(check.tokenPath);
53
- }
54
- }
55
- child.kill();
56
- throw new Error(
57
- "Kiro authentication timed out. Run `kiro-cli auth login` manually."
58
- );
59
- }
60
- };
61
92
  }
62
- }
63
- ]
93
+ ]
94
+ }
95
+ };
96
+ };
97
+ function kiroTokenPath() {
98
+ return join(homedir(), ".aws", "sso", "cache", "kiro-auth-token.json");
99
+ }
100
+ async function readKiroTokenFile(tokenPath) {
101
+ try {
102
+ const { readFile } = await import("fs/promises");
103
+ const parsed = JSON.parse(await readFile(tokenPath, "utf8"));
104
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
105
+ } catch {
106
+ return void 0;
64
107
  }
65
- });
108
+ }
66
109
  async function readToken(tokenPath) {
67
- if (tokenPath) {
110
+ const { verifyAuth } = await import("kiro-acp-ai-provider");
111
+ if (!verifyAuth().authenticated) return { type: "failed" };
112
+ const token = tokenPath ? await readKiroTokenFile(tokenPath) : void 0;
113
+ const access = typeof token?.accessToken === "string" ? token.accessToken : "";
114
+ const refresh = typeof token?.refreshToken === "string" ? token.refreshToken : "";
115
+ return {
116
+ type: "success",
117
+ refresh,
118
+ // real refresh when present, else ""
119
+ access: access || "authenticated",
120
+ // cosmetic; opencode-core only needs presence
121
+ // FUTURE expiry, refreshed every startup (server() re-runs each session) so
122
+ // opencode-core does not flag a logged-in user as expired. NOT the file value.
123
+ expires: Date.now() + 8 * 60 * 60 * 1e3
124
+ };
125
+ }
126
+ async function notifyIfTokenExpired(client) {
127
+ try {
128
+ const { verifyAuth } = await import("kiro-acp-ai-provider");
129
+ if (verifyAuth().authenticated) return;
130
+ const message = "Kiro is not logged in. Run 'kiro-cli login' to authenticate.";
131
+ console.warn(message);
132
+ void client?.tui?.showToast?.({ body: { message, variant: "warning" } })?.catch(() => {
133
+ });
134
+ } catch {
135
+ }
136
+ }
137
+ function tuiConfigPath() {
138
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
139
+ return join(base, "opencode", "tui.json");
140
+ }
141
+ async function readTuiConfig(path) {
142
+ try {
143
+ const { readFile } = await import("fs/promises");
144
+ const parsed = JSON.parse(await readFile(path, "utf8"));
145
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
146
+ } catch {
147
+ return void 0;
148
+ }
149
+ }
150
+ function isSidebarConfigured(config) {
151
+ if (!config) return false;
152
+ const plugin = config.plugin;
153
+ const enabled = config.plugin_enabled;
154
+ const hasPlugin = Array.isArray(plugin) && plugin.includes(KIRO_PLUGIN_NAME);
155
+ const sidebarOff = typeof enabled === "object" && enabled !== null && enabled[SIDEBAR_PLUGIN_ID] === false;
156
+ return hasPlugin && sidebarOff;
157
+ }
158
+ async function enableSidebarConfig(path, input) {
159
+ try {
160
+ const { readFile, writeFile, mkdir } = await import("fs/promises");
161
+ let raw;
68
162
  try {
69
- const { readFileSync } = await import("fs");
70
- const raw = JSON.parse(readFileSync(tokenPath, "utf8"));
71
- return {
72
- type: "success",
73
- refresh: "",
74
- access: raw.accessToken || "authenticated",
75
- expires: raw.expiresAt ? new Date(raw.expiresAt).getTime() : Date.now() + 36e5
76
- };
163
+ raw = await readFile(path, "utf8");
77
164
  } catch {
78
- return { type: "failed" };
165
+ raw = void 0;
79
166
  }
167
+ let config;
168
+ if (raw === void 0) {
169
+ config = {};
170
+ } else {
171
+ try {
172
+ const parsed = JSON.parse(raw);
173
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
174
+ config = parsed;
175
+ } catch {
176
+ return;
177
+ }
178
+ }
179
+ if (isSidebarConfigured(config)) return;
180
+ const plugin = Array.isArray(config.plugin) ? [...config.plugin] : [];
181
+ if (!plugin.includes(KIRO_PLUGIN_NAME)) plugin.push(KIRO_PLUGIN_NAME);
182
+ config.plugin = plugin;
183
+ const enabled = typeof config.plugin_enabled === "object" && config.plugin_enabled !== null && !Array.isArray(config.plugin_enabled) ? config.plugin_enabled : {};
184
+ enabled[SIDEBAR_PLUGIN_ID] = false;
185
+ config.plugin_enabled = enabled;
186
+ await mkdir(dirname(path), { recursive: true });
187
+ await writeFile(path, JSON.stringify(config, null, 2) + "\n", "utf8");
188
+ try {
189
+ await input.client.tui.showToast({
190
+ body: {
191
+ message: "Kiro credits sidebar enabled. Restart opencode to see it.",
192
+ variant: "success"
193
+ }
194
+ });
195
+ } catch {
196
+ }
197
+ } catch {
80
198
  }
81
- return {
82
- type: "success",
83
- refresh: "",
84
- access: "authenticated",
85
- expires: Date.now() + 36e5
86
- };
87
199
  }
88
200
  var KiroAuthPlugin = server;
89
201
  var server_default = { id: "kiro", server };
90
202
  export {
91
203
  KiroAuthPlugin,
92
- server_default as default
204
+ server_default as default,
205
+ kiroTokenPath,
206
+ notifyIfTokenExpired,
207
+ readToken
93
208
  };
package/dist/tui.d.ts CHANGED
@@ -16,6 +16,13 @@ interface PartCredits {
16
16
  interface SessionCredits {
17
17
  total: number;
18
18
  unit?: string;
19
+ /**
20
+ * True once any assistant message carried kiro credit metadata. Lets the
21
+ * sidebar/footer pick the credits view over the builtin "$X spent" fallback,
22
+ * since a credits total of 0 (a real kiro turn) is indistinguishable from
23
+ * "no kiro metadata at all" by `total` alone.
24
+ */
25
+ present: boolean;
19
26
  }
20
27
  /** Read `{ kiro: { credits, creditsUnit } }` from a part; only finite numeric credits count. */
21
28
  declare function readPartCredits(part: CreditPart): PartCredits | undefined;
package/dist/tui.js CHANGED
@@ -4,16 +4,20 @@ import {
4
4
  messageCredits,
5
5
  readPartCredits,
6
6
  sumSessionCredits
7
- } from "./chunk-JO7U4OSF.js";
7
+ } from "./chunk-JHCHA2UV.js";
8
8
 
9
9
  // src/tui.ts
10
10
  var tui = async (api) => {
11
- const { createContextView } = await import("./context-view-CGBZAYM5.js");
11
+ const { createContextView } = await import("./context-view-3MFY3ASR.js");
12
+ const { createCreditsChipView } = await import("./credits-chip-view-SKNVCWRO.js");
12
13
  api.slots.register({
13
14
  order: 100,
14
15
  slots: {
15
16
  sidebar_content(_ctx, props) {
16
17
  return createContextView(api, props.session_id);
18
+ },
19
+ session_prompt_right(_ctx, props) {
20
+ return createCreditsChipView(api, props.session_id);
17
21
  }
18
22
  }
19
23
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-kiro",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "The ACP-compliant Kiro plugin for opencode: auth via the official kiro-cli login, 12 Kiro models through the Agent Client Protocol, and TUI credits display",
5
5
  "license": "MIT",
6
6
  "author": "Nacho F. Lizaur (https://github.com/NachoFLizaur)",
@@ -42,7 +42,7 @@
42
42
  "@opencode-ai/plugin": "*"
43
43
  },
44
44
  "dependencies": {
45
- "kiro-acp-ai-provider": "^2.0.0"
45
+ "kiro-acp-ai-provider": "^2.0.2"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@opencode-ai/plugin": "1.16.2",