codeply-cli 0.2.0 → 0.3.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 (43) hide show
  1. package/THIRD_PARTY_NOTICES.md +63 -0
  2. package/bin/codeply.js +562 -370
  3. package/lib/agent.mjs +1627 -160
  4. package/lib/ai.js +1107 -387
  5. package/lib/apply-limit.js +33 -51
  6. package/lib/apply-patch.js +217 -0
  7. package/lib/arity-table.json +138 -0
  8. package/lib/arity.js +70 -0
  9. package/lib/auth.js +118 -112
  10. package/lib/commands.js +103 -0
  11. package/lib/config.js +392 -136
  12. package/lib/design-library/artifact-template.html +500 -0
  13. package/lib/design-library/assemble-artifact.mjs +21 -0
  14. package/lib/design-library/build.mjs +226 -0
  15. package/lib/design-library/cleanup.mjs +45 -0
  16. package/lib/design-library/index.json +22380 -0
  17. package/lib/design-library/make-table-data.mjs +39 -0
  18. package/lib/design-library/query.mjs +104 -0
  19. package/lib/diagnostics.js +192 -0
  20. package/lib/edit-engine.js +171 -14
  21. package/lib/github-agent.mjs +290 -0
  22. package/lib/history.js +75 -0
  23. package/lib/mcp.js +258 -0
  24. package/lib/native-tools.mjs +189 -0
  25. package/lib/oauth-connectors.js +478 -0
  26. package/lib/permissions.js +109 -0
  27. package/lib/plugins.js +353 -0
  28. package/lib/render-md.mjs +617 -617
  29. package/lib/server.mjs +457 -0
  30. package/lib/session-db.js +220 -0
  31. package/lib/share.js +168 -0
  32. package/lib/skills.js +80 -22
  33. package/lib/snapshot.js +146 -0
  34. package/lib/subagents.js +164 -0
  35. package/lib/terminal-hints.js +154 -0
  36. package/lib/textarea.mjs +258 -258
  37. package/lib/tools.mjs +1658 -79
  38. package/lib/tui.js +7 -7
  39. package/lib/tui.mjs +1086 -992
  40. package/lib/typecheck-worker.js +147 -0
  41. package/lib/web-tools.js +158 -0
  42. package/package.json +4 -3
  43. package/skills/premium-web-design/SKILL.md +154 -0
package/lib/auth.js CHANGED
@@ -1,112 +1,118 @@
1
- /**
2
- * Codeply CLI — auth
3
- *
4
- * Passwordless (email + 6-digit code) sign-in against the SAME Supabase
5
- * project the desktop app uses — same account, same ai-proxy access, same
6
- * daily request/apply caps either way. The session is cached in
7
- * ~/.codeply/auth.json, mirroring the desktop app's own file-based session
8
- * storage (there's no OS keychain dependency to add for a CLI).
9
- */
10
- const fs = require('fs');
11
- const os = require('os');
12
- const path = require('path');
13
- const readline = require('readline/promises');
14
- const { createClient } = require('@supabase/supabase-js');
15
-
16
- const SUPABASE_URL = 'https://zswkhfkfseclgadhvobg.supabase.co';
17
- const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpzd2toZmtmc2VjbGdhZGh2b2JnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODAyMzYyOTgsImV4cCI6MjA5NTgxMjI5OH0.EoTQdIGQQDrN1uEqQfya3VmrQMT68jkzPLphbLwNTWg';
18
-
19
- const configDir = path.join(os.homedir(), '.codeply');
20
- const sessionPath = path.join(configDir, 'auth.json');
21
-
22
- function ensureConfigDir() {
23
- fs.mkdirSync(configDir, { recursive: true });
24
- }
25
-
26
- // File-based storage adapter for Supabase session persistence (Node has no
27
- // localStorage) — same pattern the desktop app uses in main.js.
28
- const fileStorage = {
29
- getItem(key) {
30
- try { const d = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); return d[key] || null; }
31
- catch { return null; }
32
- },
33
- setItem(key, value) {
34
- ensureConfigDir();
35
- let d = {};
36
- try { d = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); } catch { }
37
- d[key] = value;
38
- try { fs.writeFileSync(sessionPath, JSON.stringify(d)); } catch { }
39
- },
40
- removeItem(key) {
41
- let d = {};
42
- try { d = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); } catch { }
43
- delete d[key];
44
- try { fs.writeFileSync(sessionPath, JSON.stringify(d)); } catch { }
45
- },
46
- };
47
-
48
- let client = null;
49
- function getClient() {
50
- if (!client) {
51
- client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
52
- auth: {
53
- storage: fileStorage,
54
- autoRefreshToken: true,
55
- persistSession: true,
56
- detectSessionInUrl: false,
57
- },
58
- });
59
- }
60
- return client;
61
- }
62
-
63
- async function prompt(question) {
64
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
65
- try { return (await rl.question(question)).trim(); }
66
- finally { rl.close(); }
67
- }
68
-
69
- /** Interactive email + 6-digit-code sign-in. Prints its own progress/errors. */
70
- async function login() {
71
- const supabase = getClient();
72
- const email = await prompt('Email: ');
73
- if (!email) { console.error('No email entered.'); return false; }
74
-
75
- const { error: otpErr } = await supabase.auth.signInWithOtp({ email, options: { shouldCreateUser: false } });
76
- if (otpErr) {
77
- console.error(`Could not send a code: ${otpErr.message}`);
78
- console.error('(No account with that email? Sign up in the Codeply desktop app first.)');
79
- return false;
80
- }
81
-
82
- console.log(`Code sent to ${email}.`);
83
- const code = await prompt('6-digit code: ');
84
- const { data, error } = await supabase.auth.verifyOtp({ email, token: code.replace(/\s+/g, ''), type: 'email' });
85
- if (error || !data?.session) {
86
- console.error(`Sign-in failed: ${error?.message || 'invalid code'}`);
87
- return false;
88
- }
89
-
90
- console.log(`Signed in as ${data.session.user.email}.`);
91
- return true;
92
- }
93
-
94
- async function logout() {
95
- const supabase = getClient();
96
- try { await supabase.auth.signOut(); } catch { }
97
- console.log('Signed out.');
98
- }
99
-
100
- /** Returns the current session, refreshing it if the client already has one. */
101
- async function getSession() {
102
- const supabase = getClient();
103
- const { data: { session } } = await supabase.auth.getSession();
104
- return session || null;
105
- }
106
-
107
- async function getAccessToken() {
108
- const session = await getSession();
109
- return session?.access_token || null;
110
- }
111
-
112
- module.exports = { SUPABASE_URL, SUPABASE_ANON_KEY, getClient, login, logout, getSession, getAccessToken };
1
+ /**
2
+ * Codeply CLI - auth
3
+ *
4
+ * Passwordless (email + emailed code) sign-in against the SAME Supabase
5
+ * project the desktop app uses - same account, same ai-proxy access, same
6
+ * daily request/apply caps either way. The session is cached in
7
+ * ~/.codeply/auth.json, mirroring the desktop app's own file-based session
8
+ * storage (there's no OS keychain dependency to add for a CLI).
9
+ */
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+ const readline = require('readline/promises');
14
+ const { createClient } = require('@supabase/supabase-js');
15
+
16
+ const SUPABASE_URL = 'https://zswkhfkfseclgadhvobg.supabase.co';
17
+ const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inpzd2toZmtmc2VjbGdhZGh2b2JnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODAyMzYyOTgsImV4cCI6MjA5NTgxMjI5OH0.EoTQdIGQQDrN1uEqQfya3VmrQMT68jkzPLphbLwNTWg';
18
+
19
+ const configDir = path.join(os.homedir(), '.codeply');
20
+ const sessionPath = path.join(configDir, 'auth.json');
21
+
22
+ function ensureConfigDir() {
23
+ fs.mkdirSync(configDir, { recursive: true });
24
+ }
25
+
26
+ // File-based storage adapter for Supabase session persistence (Node has no
27
+ // localStorage) - same pattern the desktop app uses in main.js.
28
+ const fileStorage = {
29
+ getItem(key) {
30
+ try { const d = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); return d[key] || null; }
31
+ catch { return null; }
32
+ },
33
+ setItem(key, value) {
34
+ ensureConfigDir();
35
+ let d = {};
36
+ try { d = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); } catch { }
37
+ d[key] = value;
38
+ try { fs.writeFileSync(sessionPath, JSON.stringify(d)); } catch { }
39
+ },
40
+ removeItem(key) {
41
+ let d = {};
42
+ try { d = JSON.parse(fs.readFileSync(sessionPath, 'utf8')); } catch { }
43
+ delete d[key];
44
+ try { fs.writeFileSync(sessionPath, JSON.stringify(d)); } catch { }
45
+ },
46
+ };
47
+
48
+ let client = null;
49
+ function getClient() {
50
+ if (!client) {
51
+ client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
52
+ auth: {
53
+ storage: fileStorage,
54
+ autoRefreshToken: true,
55
+ persistSession: true,
56
+ detectSessionInUrl: false,
57
+ // PKCE, not the (default) implicit flow - inert for this CLI's own
58
+ // email/password/OTP sign-in, but required by any client using
59
+ // signInWithOAuth + exchangeCodeForSession (e.g. the desktop app's
60
+ // and Codeply Craft's "Continue with Google" - the OAuth redirect
61
+ // carries an exchangeable code only under PKCE).
62
+ flowType: 'pkce',
63
+ },
64
+ });
65
+ }
66
+ return client;
67
+ }
68
+
69
+ async function prompt(question) {
70
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
71
+ try { return (await rl.question(question)).trim(); }
72
+ finally { rl.close(); }
73
+ }
74
+
75
+ /** Interactive email + emailed-code sign-in. Prints its own progress/errors. */
76
+ async function login() {
77
+ const supabase = getClient();
78
+ const email = await prompt('Email: ');
79
+ if (!email) { console.error('No email entered.'); return false; }
80
+
81
+ const { error: otpErr } = await supabase.auth.signInWithOtp({ email, options: { shouldCreateUser: false } });
82
+ if (otpErr) {
83
+ console.error(`Could not send a code: ${otpErr.message}`);
84
+ console.error('(No account with that email? Sign up in the Codeply desktop app first.)');
85
+ return false;
86
+ }
87
+
88
+ console.log(`Code sent to ${email}.`);
89
+ const code = await prompt('Code from the email: ');
90
+ const { data, error } = await supabase.auth.verifyOtp({ email, token: code.replace(/\D+/g, ''), type: 'email' });
91
+ if (error || !data?.session) {
92
+ console.error(`Sign-in failed: ${error?.message || 'invalid code'}`);
93
+ return false;
94
+ }
95
+
96
+ console.log(`Signed in as ${data.session.user.email}.`);
97
+ return true;
98
+ }
99
+
100
+ async function logout() {
101
+ const supabase = getClient();
102
+ try { await supabase.auth.signOut(); } catch { }
103
+ console.log('Signed out.');
104
+ }
105
+
106
+ /** Returns the current session, refreshing it if the client already has one. */
107
+ async function getSession() {
108
+ const supabase = getClient();
109
+ const { data: { session } } = await supabase.auth.getSession();
110
+ return session || null;
111
+ }
112
+
113
+ async function getAccessToken() {
114
+ const session = await getSession();
115
+ return session?.access_token || null;
116
+ }
117
+
118
+ module.exports = { SUPABASE_URL, SUPABASE_ANON_KEY, getClient, login, logout, getSession, getAccessToken };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Custom slash commands: a markdown file per command, same layout opencode
3
+ * and Claude Code use, so existing ones can be copied over.
4
+ *
5
+ * ~/.codeply/commands/<name>.md (every project)
6
+ * <project>/.codeply/commands/<name>.md (this project; same name wins)
7
+ *
8
+ * ---
9
+ * description: Review the changed files for bugs
10
+ * mode: Plan # optional: Build | Plan | Ask
11
+ * ---
12
+ * Review $ARGUMENTS for correctness bugs. Cite file:line.
13
+ *
14
+ * In the body, $ARGUMENTS is everything typed after the command, $1 $2 ...
15
+ * are single words (the highest one takes the rest), and if the body has no
16
+ * placeholder at all the arguments are appended on their own line.
17
+ * Sub-folders become part of the name: commands/git/pr.md is /git/pr.
18
+ * Installed plugins add theirs as /<plugin>:<name> (see plugins.js).
19
+ */
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+ const plugins = require('./plugins.js');
24
+
25
+ const MAX_BODY = 20000;
26
+ const RESERVED = new Set(['goal', 'g', 'skills', 'skill', 'skill-list']);
27
+
28
+ function parseFrontmatter(text) {
29
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
30
+ if (!m) return { meta: {}, body: text };
31
+ const meta = {};
32
+ for (const line of m[1].split(/\r?\n/)) {
33
+ const kv = /^([A-Za-z_-]+)\s*:\s*(.*)$/.exec(line);
34
+ if (kv) meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
35
+ }
36
+ return { meta, body: text.slice(m[0].length) };
37
+ }
38
+
39
+ function scan(dir, prefix, out) {
40
+ let entries;
41
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
42
+ for (const e of entries) {
43
+ const full = path.join(dir, e.name);
44
+ if (e.isDirectory()) scan(full, `${prefix}${e.name}/`, out);
45
+ else if (e.isFile() && e.name.toLowerCase().endsWith('.md')) {
46
+ const name = `${prefix}${e.name.slice(0, -3)}`.toLowerCase().replace(/\s+/g, '-');
47
+ if (RESERVED.has(name)) continue;
48
+ try {
49
+ const { meta, body } = parseFrontmatter(fs.readFileSync(full, 'utf8').slice(0, MAX_BODY));
50
+ const mode = ['Build', 'Plan', 'Ask'].find((x) => x.toLowerCase() === String(meta.mode || '').toLowerCase());
51
+ out.set(name, {
52
+ name,
53
+ description: meta.description || body.trim().split('\n')[0].slice(0, 100),
54
+ mode: mode || null,
55
+ body: body.trim(),
56
+ file: full,
57
+ });
58
+ } catch {}
59
+ }
60
+ }
61
+ }
62
+
63
+ /** @returns {Map<string, {name, description, mode, body, file}>} */
64
+ function loadCommands(cwd) {
65
+ const out = new Map();
66
+ for (const { plugin, dir } of plugins.commandDirs(cwd)) scan(dir, `${plugin}:`, out);
67
+ scan(path.join(os.homedir(), '.codeply', 'commands'), '', out);
68
+ if (cwd) scan(path.join(cwd, '.codeply', 'commands'), '', out);
69
+ return out;
70
+ }
71
+
72
+ /** Fill a command's template with what was typed after it. */
73
+ function expand(cmd, argText) {
74
+ const args = String(argText || '').trim();
75
+ const words = args ? args.split(/\s+/) : [];
76
+ let body = cmd.body;
77
+ const nums = [...body.matchAll(/\$(\d+)/g)].map((m) => Number(m[1]));
78
+ const max = nums.length ? Math.max(...nums) : 0;
79
+ const used = /\$ARGUMENTS|\$\d+/.test(body);
80
+ body = body.replace(/\$(\d+)/g, (_, n) => {
81
+ const i = Number(n) - 1;
82
+ return Number(n) === max ? words.slice(i).join(' ') : (words[i] || '');
83
+ });
84
+ body = body.replace(/\$ARGUMENTS/g, args);
85
+ if (!used && args) body = `${body}\n\n${args}`;
86
+ return body.trim();
87
+ }
88
+
89
+ /**
90
+ * If `text` starts with a known custom command, the expanded prompt and the
91
+ * command; otherwise null. `/goal` and the app's built-ins are never matched.
92
+ */
93
+ function resolve(text, cwd) {
94
+ const m = /^\/([A-Za-z0-9_./:-]+)(?:\s+([\s\S]*))?$/.exec(String(text || '').trim());
95
+ if (!m) return null;
96
+ const name = m[1].toLowerCase();
97
+ if (RESERVED.has(name)) return null;
98
+ const cmd = loadCommands(cwd).get(name);
99
+ if (!cmd) return null;
100
+ return { command: cmd, prompt: expand(cmd, m[2] || '') };
101
+ }
102
+
103
+ module.exports = { loadCommands, expand, resolve, parseFrontmatter };